网站搜索

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)