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.
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.