arraystypescriptobjecttuples

Create dynamic object type from two arrays/tuples of keys and values


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.


Solution

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

    Playground link to code