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 值肯定是 Foo
或 Bar
,编译器也看不到它。 key
参数属于union type"foo" | "bar"
,因此obj[key]
也是联合类型,number | string
。这些类型是正确的类型。但是 key
和 obj[key]
的类型不包含足够的信息来识别 { key, value: obj[key] }
将可分配给类型 Foo | Bar
.
如果我给你一个 "foo" | "bar"
类型的值 k
和一个 number | string
类型的值 v
,你没有理由相信 { key: k, value: v }
将是有效的 Foo
或 Bar
。 k
很可能是 "foo"
而 v
是某个字符串。
现在,我们 碰巧知道 key
和 obj[key]
相关通过分别查看它们的类型来捕获。如果key
是"foo"
,那么obj[key]
就是number
,如果key
是"bar"
,那么obj[key]
就是string
].但不幸的是,编译器只能看到单独的类型,与之前的 k
和 v
示例相同。
如果您能告诉编译器将 "foo"
情况与 "bar"
情况分开考虑,那就太好了,但这不可能发生在带有表达式的单行代码中联合类型。
关联联合类型的这个问题是 microsoft/TypeScript#30581 的主题。
TypeScript 4.6 引入了 some improvements in microsoft/TypeScript#47109 来解决这个问题。
一般的方法是重构使用 generic 函数,这样单行代码就可以只评估 "foo"
或 "bar"
。也就是说,我们在 K extends keyof Obj
中将其设为通用。我们还必须重构类型,以便编译器可以看到 Foo
和 Bar
是 K
.
的一些通用函数
这是必要的重构:
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!
}
我正在尝试 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 值肯定是 Foo
或 Bar
,编译器也看不到它。 key
参数属于union type"foo" | "bar"
,因此obj[key]
也是联合类型,number | string
。这些类型是正确的类型。但是 key
和 obj[key]
的类型不包含足够的信息来识别 { key, value: obj[key] }
将可分配给类型 Foo | Bar
.
如果我给你一个 "foo" | "bar"
类型的值 k
和一个 number | string
类型的值 v
,你没有理由相信 { key: k, value: v }
将是有效的 Foo
或 Bar
。 k
很可能是 "foo"
而 v
是某个字符串。
现在,我们 碰巧知道 key
和 obj[key]
相关通过分别查看它们的类型来捕获。如果key
是"foo"
,那么obj[key]
就是number
,如果key
是"bar"
,那么obj[key]
就是string
].但不幸的是,编译器只能看到单独的类型,与之前的 k
和 v
示例相同。
如果您能告诉编译器将 "foo"
情况与 "bar"
情况分开考虑,那就太好了,但这不可能发生在带有表达式的单行代码中联合类型。
关联联合类型的这个问题是 microsoft/TypeScript#30581 的主题。
TypeScript 4.6 引入了 some improvements in microsoft/TypeScript#47109 来解决这个问题。
一般的方法是重构使用 generic 函数,这样单行代码就可以只评估 "foo"
或 "bar"
。也就是说,我们在 K extends keyof Obj
中将其设为通用。我们还必须重构类型,以便编译器可以看到 Foo
和 Bar
是 K
.
这是必要的重构:
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!
}