Return 对象键的区分联合

Return a discriminating union from object key

我正在尝试 return 给定对象键的可区分联合:

type Foo = {
    key: 'foo';
    value: number;
};

type Bar = {
    key: 'bar';
    value: string;
};

type Obj = {
    foo: number;
    bar: string;
};

function getField(obj: Obj, key: keyof Obj): Foo | Bar {
    // Error here:
    // Type '{ key: keyof Obj; value: string | number; }' is not assignable to type 'Foo | Bar'.
    return { key, value: obj[key] };
}

当我尝试上面的代码时出现错误:Type '{ key: keyof Obj; value: string | number; }' is not assignable to type 'Foo | Bar'.

我想我需要 link obj[key] 的类型以某种方式转换为 key 的类型,但我不确定如何在不转换的情况下完成此操作。

你可以这样做:

type Foo = {
    key: 'foo';
    value: number;
};

type Bar = {
    key: 'bar';
    value: string;
};

type Obj = {
    foo: number;
    bar: string;
};

function getField(obj: Record<'foo' | 'bar', any>, key: 'foo' | 'bar'): Foo | Bar {
    return { key, value: obj[key] };
}

即使 getField() 的 return 值肯定是 FooBar,编译器也看不到它。 key参数属于union type"foo" | "bar",因此obj[key]也是联合类型,number | string。这些类型是正确的类型。但是 keyobj[key] 的类型不包含足够的信息来识别 { key, value: obj[key] } 将可分配给类型 Foo | Bar.

如果我给你一个 "foo" | "bar" 类型的值 k 和一个 number | string 类型的值 v,你没有理由相信 { key: k, value: v } 将是有效的 FooBark 很可能是 "foo"v 是某个字符串。

现在,我们 碰巧知道 keyobj[key] 相关通过分别查看它们的类型来捕获。如果key"foo",那么obj[key]就是number,如果key"bar",那么obj[key]就是string ].但不幸的是,编译器只能看到单独的类型,与之前的 kv 示例相同。

如果您能告诉编译器将 "foo" 情况与 "bar" 情况分开考虑,那就太好了,但这不可能发生在带有表达式的单行代码中联合类型。

关联联合类型的这个问题是 microsoft/TypeScript#30581 的主题。


TypeScript 4.6 引入了 some improvements in microsoft/TypeScript#47109 来解决这个问题。

一般的方法是重构使用 generic 函数,这样单行代码就可以只评估 "foo""bar"。也就是说,我们在 K extends keyof Obj 中将其设为通用。我们还必须重构类型,以便编译器可以看到 FooBarK.

的一些通用函数

这是必要的重构:

type FooBar<K extends keyof Obj> = { [P in K]: { key: P, value: Obj[P] } }[K];
type Foo = FooBar<"foo">;
type Bar = FooBar<"bar">;

FooBar<K> 类型是 分布式对象类型 ,如 microsoft/TypeScript#47109 中创造的那样。类型FooBar<"foo">和你原来的Foo一样,FooBar<"bar">Bar一样。 FooBar<keyof Obj>Foo | Bar 相同。

现在你可以这样写 getField():

function getField<K extends keyof Obj>(obj: Obj, key: K): FooBar<K> {
  return { key, value: obj[key] }; // okay
}

并验证它是否按预期工作:

const obj: Obj = { foo: 1, bar: "x" };
const f: Foo = getField(obj, "foo");
const b: Bar = getField(obj, "bar");

万岁!


请注意:重要的是 FooBar 是根据通用 {key: P, value: Obj[P]} 编写的,而 getField() 是用类似的 {key: key, value: obj[key]} 实现的。如果将 FooBar 重写为与 Obj 类型无关,例如

type FooBar<K extends keyof Obj> = Extract<Foo | Bar, { key: K }>;

然后你会再次得到同样的错误:

function getField<K extends keyof Obj>(obj: Obj, key: K): FooBar<K> {
  return { key, value: obj[key] }; // error!
}

Playground link to code