Home > Mobile >  How to use variable name as object reference in typescript/react
How to use variable name as object reference in typescript/react

Time:07-22

I have an interface called GenerationInterface:

export default interface Generation{
  id?: number;
  name?: string;
  latitude?: string;
  longitude?: string;
  source?: string;
  max_power?: number;
  current_power?: number;
}

I'm using MUI input fields like this:

                    <TextField
                      margin="dense"
                      id="latitude"
                      label="Latitude"
                      type="text"
                      name="latitude"
                      fullWidth
                      variant="standard"
                      value={currentGeneration.latitude}
                      onChange={handleGenerationFormChange}
                    />

Ignoring the fact that some of the elements are numeric and some are strings - let's pretend they're all strings - how can I use the passed name as a key for the interface to set elements of the interface with elegant code? This is my attempt:

    const handleGenerationFormChange = (event:React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
      let genHolder = currentGeneration;

      genHolder[event.target.name as keyof GenerationInterface] = event.target.value;
      
    }

However, this throws the following error:

TS2322: Type 'string' is not assignable to type 'undefined'.

What's the right way to do it?

CodePudding user response:

You could try using a generic parameter:

    const handleGenerationFormChange = <T extends keyof GenerationInterface>(event:React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
      let genHolder = currentGeneration;

      genHolder[event.target.name as T] = event.target.value as GenerationInterface[T];
    }

CodePudding user response:

To do the assignment, you have to reassure TypeScript that the key you're trying to use identifies a string property in Generation.

There are a couple of ways to do that.

Generic Extracting String/Number Properties and Type Predicates

You can use a generic like this to get the keys for properties that are assignable to a given type:

type AssignableKeys<Source extends object, Target> = Exclude<{
    [Key in keyof Source]: Required<Source>[Key] extends Target ? Key : never;
}[keyof Source], undefined>

(More about how that works in my answer here, which is derived [no pun!] from this answer by Titian Cernicova-Dragomir.)

Then a type predicate to narrow the type of the string key you're using (this version repeats property names, but keep reading); notice the AssignableKeys<Generation, string> type at the end, saying the key identifies a string-typed property of Generation:

function isGenerationStringKey(key: string): key is AssignableKeys<Generation, string> {
    switch (key) {
        case "name":
        case "latitude":
        case "longitude":
        case "source":
            return true;
        default:
            return false;
    }
}

...and/or a type assertion function that asserts the key is valid:

function assertIsGenerationStringKey(key: string): asserts key is AssignableKeys<Generation, string> {
    if (!isGenerationStringKey(key)) {
        throw new Error(`Key "${key}" doesn't specify a string-typed Generation property`);
    }
}

Then your function can use the type predicate to narrow the type of the key:

const handleGenerationFormChange = (event:React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
    let genHolder = currentGeneration;
    const key = event.target.name;
    if (isGenerationStringKey(key)) {
        genHolder[key] = event.target.value;
    }
};

or with the type assertion function:

const handleGenerationFormChange = (event:React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
    let genHolder = currentGeneration;
    const key = event.target.name;
    assertIsGenerationStringKey(key);
    genHolder[key] = event.target.value;
};

Repeat it for the numeric properties; perhaps you'd have a separate event handler for those that converts value to number and uses a number-oriented type predicate.

Playground link

Example Objects and Derived Types (and Type Predicates)

Often when I want names at compile-time and also at runtime (for instance, for type predicates), I do it by having example objects and deriving the type from them. In your case:

// The example objects
// NOTE: You can put documentation comments on these properties,
// and the comments will be picked up by the derived types
const generationStrings = {
    name: "",
    latitude: "",
    longitude: "",
    source: "",
};
const generationNumbers = {
    id: 0,
    max_power: 0,
    current_power: 0,
};

// Deriving types:
type GenerationStringProperties = typeof generationStrings;
type GenerationNumberProperties = typeof generationNumbers;
type Generation = Partial<GenerationStringProperties & GenerationNumberProperties>;

The type predicate and/or type assertion function is simpler and doesn't repeat names

function isGenerationStringKey(key: string): key is keyof GenerationStringProperties {
    return key in generationStrings;
}
function assertIsGenerationStringKey(key: string): asserts key is keyof GenerationStringProperties {
    if (!isGenerationStringKey(key)) {
        throw new Error(`Key "${key}" doesn't specify a string-typed Generation property`);
    }
}

The usage in the event handler is the same as above.

Playground link

  • Related