I have an array of keys and a tuple of values types.
const keys = ["name", "age", "isAlive"] as const;
type Values = [string, number, boolean];
How can I use them to dynamically construct the following type?
type DesiredType = {
name: string;
age: number;
isAlive: boolean;
}
I have tried:
const keys = ["name", "age", "isAlive"] as const;
type Values = [string, number, boolean];
type ObjFromArrays<K extends readonly string[], V extends any[]> = {
[I in K[number]]: V[number];
};
type DesiredType = ObjFromArrays<typeof keys, Values>;
But it doesn't work because it iterates on K
but not on V
.
Essentially you want ObjFromArrays<K, V>
to take two data types of the same shape, where the first one K
holds keys, and the second one V
holds values, and you want to join them into a single object type where each key is paired with each value. The way to do this is by writing a mapped type over the relevant indices I
of the K
data type, and using key remapping to use K[I]
as the key, and V[I]
as the value. Assuming K
is a tuple type, then the relevant indices are the numeric-like string keys of K
corresponding to positions in the tuple. That is, if K
looks like ["name", "age", "isAlive"]
, then the indices we want to iterate over is "0" | "1" | "2"
.
Here's how we implement that:
type ObjFromArrays<
K extends readonly string[],
V extends Record<keyof K, any>
> = {
[I in `${number}` & keyof K as K[I]]: V[I];
};
Here K
has been constrained to a (readonly) array of strings, while V
has been constrained to have the same indices as K
. You could write V extends readonly string[]
also, but then you'd need to check inside your implementation that each index I
of K
was also a valid index of V
, and do something if not. We want to prevent ["name", "age", "isAlive"]
for K
and something like [string]
for V
, where the indices don't match. Instead, by constraining to Record<keyof K, any>
we're basically requiring that V
have at least the same indices as K
.
The mapped type iterates over the keys of K
(keyof K
) which are also (&
) numeric-like strings. (`${number}`
). The reason why I intersect with `${number}`
is to avoid iterating over all the apparent keys of arraylike types, such as "length"
and "reduce"
and "forEach"
, etc. For each such key I
, the key of the output type is K[I]
and the value is V[I]
.
Let's test it out:
const keys = ["name", "age", "isAlive"] as const;
type Values = [string, number, boolean];
type DesiredType = ObjFromArrays<typeof keys, Values>;
/* type DesiredType = {
name: string;
age: number;
isAlive: boolean;
} */
Looks good.