Search code examples
typescripttypescript-types

Is it possible to map types with keys from objects in a tuple


I need to (if possible) map types in TypeScript in a way where I have a tuple consisting of { readonly name: string, readonly type: 'string' | 'number' } objects, and I want to map it to a type with the name as keys and their corresponding type (mapped to an actual type) as their type.

For example, for an input of

const input = [
  { name: "foo", type: "string" },
  { name: "bar", type: "number" }
] as const

I need to get a type looking like this:

type ExpectedOutput = {
  foo: string,
  bar: number
}

So I'm searching for a type mapping X such that X<typeof input> matches ExpectedOutput.

Mapping type is pretty straight-forward:

type Type = "string" | "number";

type MapType<T extends Type> = T extends "string"
  ? string
  : number

With that, mapping a single tuple entry is easy enough (and works as expected):

type X_1<T extends { readonly name: string; readonly type: Type }> = {
  [K in T["name"]]: MapType<T["type"]>;
};

const example: X_1<{ name: "age"; type: "number" }> = {
  age: 42
}; // correct
const example2: X_1<{ name: "age"; type: "number" }> = {
  age: "42"
}; // type error

It's only when I try to have a tuple of values where I run into issues:

type X<T extends readonly { readonly name: string; readonly type: Type }[]> = {
  [K in T[number]["name"]]: MapType<T[number]["type"]>;
};

const tuple = [{ name: "age", type: "number" }, { name: "name", type: "string" }] as const;

const example3: X<typeof tuple> = {
  age: 42,
  name: "John"
}; // correct
const example4: X<typeof tuple> = {
  age: "42",
  name: "John"
}; // should have a type error, but doesn't

Whatever I try to do (I've tried dozens of variations), the properties always get evaluated as any of the tuple's types (in this example, string | number), which makes sense due to [number] returning the union of all possible options.

It's been too long since I've last done deep "black magic" with TypeScript types, so I'm not quite sure if this is even possible.

So my question(s) would be: Is there a way to get this to map to the correct types based on the tuple? If so: how? Thank you very much in advance for any help/pointers.

PS: The background of this question (which I left out for the sake of simplicity) is that I'm building a library that, given a specific set of parameters, should build a zod schema that resolves to the correct type after validation.


Solution

  • In your version of the mapped type (and presumably the dozens of variations) the property value type does not mention the property key type K, so there's no hope that it will depend on the particular key. That is, your key K iterates over the union type T[number]["name"], but the value involves T[number]["type"] which is another union type, and there's no relationship to K, producing

    const tuple = [
        { name: "age", type: "number" },
        { name: "name", type: "string" }
    ] as const;
    type Z = X<typeof tuple>;
    /* type Z = {
        age: string | number;
        name: string | number;
    } */
    

    One way to fix this is to use key remapping in mapped types so that instead of iterating K over T[number]["name"], you iterate a type parameter U over T[number] and use U["name"] as the key and U["type"] to produce the value:

    type X<T extends readonly { readonly name: string; readonly type: Type }[]> = {
        [U in T[number] as U["name"]]: MapType<U["type"]>;
    };
    

    Now when you use it you get:

    const tuple = [
        { name: "age", type: "number" },
        { name: "name", type: "string" }
    ] as const;
    type Z = X<typeof tuple>
    /* type Z = {
        age: number;
        name: string;
    }*/
    

    That's the type you wanted.

    Playground link to code