Home > Back-end >  TypeScript: Return correct type for object property by key
TypeScript: Return correct type for object property by key

Time:03-09

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

TypeScript playground here: https://www.typescriptlang.org/play?#code/MYewdgzgLgBAJgQygmBeGBvAUDBAbAS2AFMAuTPEAN2InIGYBfAGhwCMQ3yNKa6YA5AAsCAB1EgIAljmBCEAJ0JlM8qLXICoIBNGmt4xMAQgqe1DTAAUCUmACuAWzbEFASjQA GAAMhxPEofRixGXAgYUEgoAG4seIAzezBgKAJwGFFXCHAAGQsIKzAERzIAa2IATxAEmChKrJr4JAQPbBgOyPBoGAJ1RzRm5ABtYtKAXRxOqJ7eWkGBOalesF7 mAB NeJHADolmHJkuGIEgjBiODjOmAViKHsFVaW4kKwZ2HwiYny QayFDkwL9aFYBF8SAI3HEAPQwmAASRgAHcEGBYFARBFtDAXHUGsQYA5nK5djB4h9cZwQRF0ACgTSwRw2FDYfCkaj0XUsXUQLjCfUsjBoApzgBzGAgBSCETiEACCndWByRTKGn-bJ5ApglVKAjEVlYOGIlFojE8nF4wWE46nc6XRXRQzGUzquma4HagQnF0G6FG9mmrmYky8-n4oU2OxOFzuLzCqCisBioA

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

Typescript Playground

  • Related