0

I found this clever example of using a single change function to handle multiple text inputs and state changes for React and Typescript. The example, naturally, uses a normal string type:


type MyEntity = {
   name: string;
  // would work with any other fields
}

const handleChange = (fieldName: keyof MyEntity) => (
    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    setValues({ ...values, [fieldName]: e.currentTarget.value });

However, my type for strings is a dictionary. Eg:

{[index: string]: string}

...where the index is a language code string and its corresponding value is a string for that language's content. For example, a name object would look like, name:{'en': 'Michael', 'es': 'Miguel'}

Here's my current field-specific change handler that I'd like to make work for all text fields. Note- selectedLanguage is a string state variable with the language code the user selected, omitted for brevity. See codesandbox link below for full version):


type StringLanguage = {
  [index: string] : string;
}

interface MyEntity {
  name: StringLanguage;
}

const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setData((prevData) => {
      return {
        ...prevData,
        name: {
          ...prevData.name,
          [selectedLanguage]: e.target.value
        }
      };
    });
  };

Here's my attempt to use one like the example I found.

  const handleChange2 = (fieldName: keyof MyEntity) => (
    e: React.ChangeEvent<HTMLInputElement>
  ) => {
    
    setData((prevData) => { 
      return {
      ...prevData, 
      [fieldName]: {
        ...prevData[fieldName],
        [selectedLanguage]: e.currentTarget.value 
        }
      };
    });
  };

TS is giving me the error, "Spread types may only be created from object types. ts(2968)" on the line of code: ...prevData[fieldName]

But prevData is an object and in the field-specific version it works. How do I get this more generic function to work with my dictionary type, StringLanguage?:

Here's the usage for a text input:

onChange={(e) => handleChange2("name")}

Finally, here is a codesandbox with the complete example of what I'm doing. Feel free to comment out code to experiment. I've left the version that works active and commented out what doesn't work from above:

example

MattoMK
  • 609
  • 1
  • 8
  • 25
  • Are you sure `prevData[fieldName]` is an object? The error says that it's not. – DemiPixel Oct 07 '21 at 17:54
  • I'm sure the error is correct, but I don't understand it. In the working example, I'm specifying the literal "name" to reference the new name object, and when grabbing the current value of the 'name' object. In my mind the fieldName is a placeholder for the 'name' literal. But obviously I'm missing something. – MattoMK Oct 07 '21 at 18:18
  • No no, the error is correct, I just mean that the error is stating that `prevData[fieldName]` is not an object. Is `prevData` really of type `MyEntity`? – DemiPixel Oct 07 '21 at 18:22
  • Ok. Yes, prevData is MyEntity. If I hover over it in the handler the compiler indicates it is of that type. – MattoMK Oct 07 '21 at 18:26
  • Is every single property in `MyEntity` an object? If `name` is, but, say, `age` isn't, then it's throwing an error because `fieldName` could be `age`. What is the type of `prevData[fieldName]`? – DemiPixel Oct 07 '21 at 18:28
  • Ah... no, my actual code locally here has multiple members of various types on the entity (as does the sandbox). I think I'm starting to get what you're alluding to (as I attempt to think from TS's point of view). – MattoMK Oct 07 '21 at 18:42

1 Answers1

1

Hey the thing is that in your interface MyEntity there is an id: string property, that is why the ts complier complains.

If you want to do something generic it would be something like this

  const isPrimitive = (yourVariable: any) =>
    typeof yourVariable === "object" && yourVariable != null;

  const overrideObject = (original: object, override: object) => ({
    ...original,
    ...override
  });

  const handleChange = (fieldName: keyof MyEntity) => (
    e: React.ChangeEvent<HTMLInputElement>
  ) => {
    setData((prevData: MyEntity) => {
      return {
        ...prevData,
        [fieldName]: isPrimitive(prevData[fieldName])
          ? e.currentTarget.value
          : overrideObject(prevData[fieldName] as object, {
              [selectedLanguage]: e.currentTarget.value
            })
      };
    });
  };
jbheber
  • 84
  • 4
  • Thank you! No TS error. But the input is not reflecting anything when you type. In your primitive function where you're saying ```typeof yourVariable === 'object'```, did you mean !== object? or === 'string'? A primitive here means MyEntity.Id right? I tried changing isPrimitive and removing your return handler code into a getData function and passed data.name and data.id and some random values to simulate target.value and it does return the expected data. But in the context of the change handler it still doesn't work. I made Example2.tsx in the sandbox. – MattoMK Oct 07 '21 at 19:50
  • 1
    Yep, what I was trying to show is that for primitive values (not just string) just override with the new data, and for different types of objects do their respective destructuring `{...obj }` || `[...arr]`. Also, notice that `handleChange` is a function returning a function... so the way of you using it is `handleChange("name")(e)` – jbheber Oct 08 '21 at 13:39
  • So in your code it would be `onChange={(e) => handleChange("name")(e as any)}` or `onChange={handleChange("name")}` – jbheber Oct 08 '21 at 13:42