typescripttypescript-genericstype-inferencetemplate-literalstypescript-template-literals

How to ensure object values are keys of a type deduced by the object key with TypeScript?


Given

// GIVEN

type Animal<T extends string> = {
    id: T,
}

type Dog = Animal<"animal.dog"> & {
   foo: string
}

type Cat = Animal<"animal.cat"> & {
   bar: string
}

type MyType<T extends Dog | Cat = Dog | Cat> = { [K in T["id"]]: keyof T };

type MyTypeVariant<C extends "animal.dog" | "animal.cat" = "animal.dog" | "animal.cat"> = { [K in C]: keyof Animal<C> };

// EXPECT TO BE OK

const obj: MyType = {
    "animal.dog": "foo",
    "animal.cat": "bar"
}

// EXPECT TO FAIL

const fail: MyType = {
    "animal.dog": "bar",
    "animal.cat": "foo"
}

How can I create a type that would raise an error if the value (ex: "foo") is not a key of Type (ex: Cat), Type that can be deduced from the key (ex: "animal.cat") of obj?

For now I get the error

Type '"foo"' is not assignable to type '"id"'

Because "id" is the only property in common between Dog & Cat.

I guess I would need type inference here.

Any ideas?


Solution

  • The type you're looking for is

    type Type =
      { [T in Dog | Cat as T["id"]]: keyof T }
    

    which evaluates to

    type Type = {
        "animal.dog": "id" | "foo";
        "animal.cat": "id" | "bar";
    }
    

    This uses key remapping in mapped types to iterate over the union Dog | Cat, and for each member T of that union, we use T["id"] as the key, and keyof T as the value. Since we're iterating over the members of T, then the correlation between key and value is preserved.


    If we needed to do it your way, where we iterate K over T["id"] instead of T, we'd need to check K and Extract the right member of T, perhaps like this:

    type TypeGen<T extends Dog | Cat = Dog | Cat> =
      { [K in T["id"]]: keyof Extract<T, { id: K }> }
    
    type Type = TypeGen
    /* type Type = {
        "animal.dog": "id" | "foo";
        "animal.cat": "id" | "bar";
    } */
    

    But key remapping is more straightforward.

    Playground link to code