I have a const object with known keys. Each item may or may not have a specific property. I want a function to, given the key, return the value along with it's appropriate type.
I'm using TypeScript 4.6.2.
Of course I can just type in data.key?.property
but I need to do this programatically.
Here's a contrived example to show what I'm after:
const data = {
alice: {loves: 3},
bob: {loves: 'hippos'},
charlie: {hates: 'toast'},
denise: {loves: (a:number) => `hello`}
} as const;
function personLoves(name:keyof typeof data) {
const item = data[name]
const loves = 'loves' in item ? item.loves : undefined;
// ^ this line is clearly not right!
return loves;
}
const aliceLoves = personLoves('alice');
// I want `aliceLoves` to be type number.
const bobLoves = personLoves('bob');
// I want `bobLoves` to be type string or 'hippo'
const charlieLoves = personLoves('charlie');
// I want `charlieLoves` to be type undefined
const deniseLoves = personLoves('denise');
// I want `deniseLoves` to be type (a:number) => string
CodePudding user response:
In this situation I'd recommend using a generic signature on personLoves
to capture the idea that name
is a specific key of data
. That will allow TypeScript to track the correct associated return type based on the value you pass when you invoke the function.
In combination with that, I would use an overload signature to capture the two situations you have: when the data for the person contains a loves
key (in which case we infer the return type from the type of that key's value), and when it does not (in which case the return type is always undefined
).
// A helper type that we will need to figure out which keys have values with a 'loves' property
type KeysExtending<O, T> = {
[K in keyof O]: O[K] extends T ? K : never;
}[keyof O];
// Then, the keys of `data` which DO have a 'loves' key are:
type KeysWithLoves = KeysExtending<typeof data, {loves: unknown}> // resolves to: "alice" | "bob" | "denise"
// Then, write one overload case for keys in the above type, and another for everything else.
// Since overloads always try to match to the earliest listed overload, this will first capture the properties that DO have a 'loves' key, and infers the correct return type from that.
// Anything not caught by the first overload must be a name without a 'loves' key in its data, so we give it return type 'undefined'.
function personLoves<T extends KeysWithLoves>(name: T): (typeof data)[T]["loves"];
function personLoves(name: keyof typeof data): undefined;
function personLoves(name: keyof typeof data) {
const item = data[name]
const loves = 'loves' in item ? item.loves : undefined;
return loves;
}
This seems to get the result you desire:
const aliceLoves = personLoves('alice');
// Is the literal number type: 3.
const bobLoves = personLoves('bob');
// Is the literal string type: 'hippo'
const charlieLoves = personLoves('charlie');
// Is the type: undefined
const deniseLoves = personLoves('denise');
// Is the type: (a:number) => string