Search code examples
typescriptgenericstypescript-typingstypescript-generics

How to use Typescript Generics to infer type of object property


Given this simplified example code:

type People = {
    [key: string]: {
        names: (string | number)[];
    };
};

const people: People = {
    bert: {
        names: ["Bert", 1],
    },
    ernie: {
        names: ["Ernie", 2, "The Ern"],
    },
};

function getNames(person: People[string]) {
    return person.names;
}

const names = getNames(people.bert); // names: string | number

In this case, I would like names to be of type "Bert" | 1. So it should be inferred from the names property of whoever I pass into getNames.

I am unable to figure out how I can use Typescript Generics to fix this. Can somebody help me with this? Many thanks.

#edit

After the answer of @ caleth, i am thinking of an example closer to the actual use-case, I am wondering if a simpler solution exists.

When i have this code:

const tests = {
    test1: {
        variants: ["variant1", "variant2"],
    },
    // ...
}

function getRandomVariant(test) {
    // some random logic
    return test.variants[0];
}

const variant = getRandomVariant(tests.test1);
// variant must be of type "variant1" | "variant2"

if(variant == "variant1") {
    // ...
} else if(variant == "variant2") {
    // ...
}

How to make this typesafe and use generics to get variant to be of type "variant1" | "variant2"?


Solution

  • First off you need to make sure that people is inferred to a type where people.bert.names still has that type, which means changing the definition of people:

    const people = {
        bert: {
            names: ["Bert", 1],
        },
        ernie: {
            names: ["Ernie", 2, "The Ern"],
        },
    } as const satisfies People;
    

    Then we can write a helper type to pull out the element type from names:

    type Names<T> = T extends { names: readonly (infer R)[]; } ? R : never;
    

    Then we can put it all together with typing getFirstName:

    function getFirstName<T extends { names: any }>(person: T): Names<T> {
        return person.names[0];
    }
    

    Playground link