Búsqueda de sitios web

typescript - Devolver el tipo correcto para la propiedad de un objeto por clave


Tengo un objeto const con claves conocidas. Cada elemento puede o no tener una propiedad específica. Quiero una función que, dada la clave, devuelva el valor junto con su tipo adecuado.

Estoy usando TypeScript 4.6.2.

Por supuesto que puedo simplemente escribir data.key?.property, pero necesito hacer esto de manera programática.

Aquí hay un ejemplo artificial para mostrar lo que busco:

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

Patio de recreo

Respuesta #1

En esta situación, recomendaría usar una firma genérica en personLoves para capturar la idea de que name es una clave específica de data. Eso permitirá que TypeScript rastree el tipo de retorno asociado correcto basado en el valor que pases cuando invoques la función.

En combinación con eso, usaría una firma de sobrecarga para capturar las dos situaciones que tienes: cuando los datos de la persona contienen una clave loves (en cuyo caso inferimos el tipo de retorno a partir del tipo del valor de esa clave), y cuando no la contienen (en cuyo caso el tipo de retorno es siempre 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;
}

Esto parece obtener el resultado que deseas:

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

Área de juego de Typescript

Respuesta #2

Necesitarás crear al menos un tipo intermedio, LovesType, que pueda inferir el tipo del valor loves a partir del objeto data.

type LovesType<T> = T extends { loves: infer L } ? L : undefined;

function personLoves<T extends keyof typeof data>(name: T): LovesType<typeof data[T]> {
  const person = data[name];
  return ('loves' in person ? person.loves : undefined) as LovesType<typeof data[T]>;
}

Mejorada legibilidad

También puedes introducir un tipo intermedio llamado Person que represente una entrada en el objeto data. Esto ayuda a evitar repetir typeof data[T] y reduce la redundancia en las definiciones de tipo.

type Person = typeof data[keyof typeof data];
type LovesType<T> = T extends { loves: infer L } ? L : undefined;

function personLoves<T extends keyof typeof data>(name: T): LovesType<Person> {
  const person = data[name] as Person;
  return ('loves' in person ? person.loves : undefined) as LovesType<Person>;
}

Ejemplo completo:

Aquí está el ejemplo completo en funcionamiento que demuestra el uso de ambos tipos intermedios (Person y LovesType):

const data = {
  alice: { loves: 3 },
  bob: { loves: 'hippos' },
  charlie: { hates: 'toast' },
  denise: { loves: (a: number) => `hello` }
} as const;

type Person = typeof data[keyof typeof data];
type LovesType<T> = T extends { loves: infer L } ? L : undefined;

function personLoves<T extends keyof typeof data>(name: T): LovesType<Person> {
  const person = data[name] as Person;
  return ('loves' in person ? person.loves : undefined) as LovesType<Person>;
}

const aliceLoves = personLoves('alice');
console.log(aliceLoves); // 3 (number)

const bobLoves = personLoves('bob');
console.log(bobLoves); // 'hippos' (string)

const charlieLoves = personLoves('charlie');
console.log(charlieLoves); // undefined

const deniseLoves = personLoves('denise');
console.log(deniseLoves); // (a: number) => string (function)