1

I have an interface called GenerationInterface:

export default interface GenerationInterface{
  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?

2
  • Generation or GenerationInterface? You seem to be using them interchangeably. Commented Jul 22, 2022 at 13:04
  • typo. I've fixed it. Commented Jul 22, 2022 at 14:42

2 Answers 2

2

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

Sign up to request clarification or add additional context in comments.

3 Comments

Does this apply in the case I stipulated in my question? I said, "forget that some of these are numbers, just assume they're all strings."
This is otherwise very helpful. I'm still struggling with the basic question of "at what point is typescript going to understand that a thing is a string". It seems like sometimes when I wrap code with an if statement testing it, it doesn't seem to take. I guess I just have a lot to learn about typescript.
@froopydoop - Yes, this applies to your case, I just did make sure to have the escape clause for numbers as well since they were there and I figure you did want to handle them. I mostly ignored the fact some of them were numbers, but called out that you'll probably want to have a parallel event handler for the number case that uses a type predicate for keys that identify number properties. If all of the properties had string values, you could use keyof Required<GenerationInterface> or similar, but since your real interface had numbers, I didn't want to provide a half-solution.
2

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];
    }

2 Comments

The problem with unchecked type assertions is that, almost inevitably, you end up getting them wrong. :-)
If I do that, genholder is no longer of type GenerationInterface, right?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.