typescript - 通过键返回对象属性的正确类型
我有一个带已知键的 const 对象。每个条目可能有也可能没有特定属性。我想要一个函数,给定键时,返回该值及其相应类型。
我正在使用 TypeScript 4.6.2。
当然我可以直接输入 data.key?.property,但我需要以编程方式执行此操作。
这是一个人为的例子,用来展示我所追求的内容:
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
操场
答案#1
在这种情况下,我建议在 personLoves 上使用通用签名,以捕捉 name 是 data 的一个 特定 键的想法。这将允许 TypeScript 根据你调用函数时传递的值跟踪正确的相关返回类型。
结合这一点,我会使用重载签名来捕捉你遇到的两种情况:当某人的数据包含 loves 键时(此时我们从该键的值的类型推断返回类型),以及当它不包含时(此时返回类型始终为 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;
}
这似乎可以得到你想要的结果:
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 游乐场
答案#2
你需要至少创建一个中间类型,LovesType,它可以从 data 对象中推断 loves 值的类型。
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]>;
}
可读性提升
你也可以引入一个名为 Person 的中间类型,它表示 data 对象中的一条条目。这有助于避免重复使用 typeof data[T] 并减少类型定义中的冗余。
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>;
}
完整示例:
这是一个完整的工作示例,演示了两种中间类型(Person 和 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)
