This is kind of a theoretical question but I was wondering if we can build an input type that checks if a given enum key if passed to as a key to an object resolves to an array. I guess this is better explained with an example:
enum FormKeys {
a = "a",
b = "b",
}
interface IInitalFormValues {
[FormKeys.a]: number
[FormKeys.b]: string[]
}
const initialFormValues: IInitalFormValues = {
[FormKeys.a]: 123,
[FormKeys.b]: ["a"],
}
function onChange(input: FormKeys) {
/*
Error here since if we pass FormKeys.a then assigning an array to
type number is an error
*/
initialFormValues[input] = []
}
onChange(FormKeys.b)
What we can obviously do here is to change input: FormKeys
to input: FormKeys.b
but this solution does not scale well, is there a way to do it more generically?
Thanks
CodePudding user response:
You can pick out the keys that are for array types by doing this (I picked this up here):
type ArrayKeys<T> = {
[Key in keyof T]: T[Key] extends any[] ? Key : never;
}[keyof T];
(That's a bit tricky, full explanation below.) ArrayKeys<IInitialFormValues>
would be just FormKeys.b
, for instance, since that's the only property with an array type. If you had more, ArrayKeys<IInitialFormValues>
would be a union of them.
Then your input
parameter's type is ArrayKeys<IInitialFormValues>
:
function onChange(input: ArrayKeys<IInitialFormValues>) {
initialFormValues[input] = [];
}
That way, this works:
onChange(FormKeys.b); // Works as desired
but this fails:
onChange(FormKeys.a); // Fails as desired
How that ArrayKeys
works:
type ArrayKeys<T> = {
[Key in keyof T]: T[Key] extends any[] ? Key : never;
}[keyof T];
There are two stages to that:
The
{/*...*/}
part maps each property ofT
into a new mapped type where the type of the property is the key itself ornever
. Suppose we had:type Example = { a: string[]; b: number[]; c: string; };
Just the first part of
ArrayKeys
creates this anonymous type:{ a: "a"; b: "b"; c: never; }
Then the
[keyof T]
part at the end creates a union of the types of those properties, which in theory would be"a" | "b" | never
, butnever
is always dropped from union types, so we end up with"a" | "b"
— the keys ofExample
for properties with array types.
You can go a step further and build a type consisting of only the parts of Example
that match that by using Pick<Example, ArrayKeys<Example>>
, but you don't need that for this specific purpose.