类型化对象联合的详尽映射
Exhaustive map over a union of typed objects
我希望 TypeScript 在像这样映射联合时强制执行穷举:
type Union =
{ type: 'A', a: string } |
{ type: 'B', b: number }
联合事件处理器:
const handle = (u: Union): string =>
theMap[u.type](u);
如果我们能以某种方式从 TypeScript 获得详尽检查,那就太好了:
const theMap: { [a: string]: (u: Union) => string } = {
A: ({a}: { type: 'A', a: string }) => 'this is a: ' + a,
B: ({b}: { type: 'B', b: number }) => 'this is b: ' + b
};
TS2.8+更新
因为conditional types were released, the manipulation needed to strongly type theMap
has gotten a lot easier. Here we will use Extract<U, X>
采用联合类型U
和return只有那些可分配给X
的成分:
type Union = { type: "A"; a: string } | { type: "B"; b: number };
const theMap: {
[K in Union["type"]]: (u: Extract<Union, { type: K }>) => string
} = {
A: ({ a }) => "this is a: " + a,
B: ({ b }) => "this is b: " + b
};
超级简单!不幸的是,自 TS2.7 左右以来,编译器不再允许您调用 theMap(u.type)(u)
。函数 theMap(u.type)
与值 u
相关,但编译器看不到。相反,它将 theMap(u.type)
和 u
视为独立的联合类型,并且不会让您在没有类型断言的情况下调用另一个:
const handle = (u: Union): string =>
(theMap[u.type] as (v: Union) => string)(u); // need this assertion
或不手动遍历可能的联合值:
const handle = (u: Union): string =>
u.type === "A" ? theMap[u.type](u) : theMap[u.type](u); // redundant
我通常建议人们为此使用断言。
我有一个关于此类 correlated types 的未决问题,但我不知道是否会支持它。无论如何,再次祝你好运!
TS2.7及以下答案:
鉴于定义的类型 Union
,很难(或者可能不可能)哄骗 TypeScript 为您提供一种表达详尽性检查的方法(theMap
只包含一个用于每个成分的处理程序联合的类型)和稳健性约束(theMap
中的每个处理程序都是针对联合的 特定 组成类型的。
但是,可以根据更一般的类型来定义 Union
,您也可以从中表达上述约束。先来看比较通用的类型:
type BaseTypes = {
A: { a: string };
B: { b: number };
}
这里,BaseTypes
是从原来的Union
的type
属性到去掉type
的构成类型的映射。由此,Union
等价于({type: 'A'} & BaseTypes['A']) | ({type: 'B'} & BaseTypes['B'])
。
让我们在类型映射上定义一些操作,例如 BaseTypes
:
type DiscriminatedType<M, K extends keyof M> = { type: K } & M[K];
type DiscriminatedTypes<M> = {[K in keyof M]: DiscriminatedType<M, K>};
type DiscriminatedUnion<M, V=DiscriminatedTypes<M>> = V[keyof V];
您可以验证 Union
等同于 DiscriminatedUnion<BaseTypes>
:
type Union = DiscriminatedUnion<BaseTypes>
此外,定义 NarrowedFromUnion
:
也很有帮助
type NarrowedFromUnion<K extends Union['type']> = DiscriminatedType<BaseTypes, K>
它采用一个键 K
并生成与该 type
的并集的组成部分。所以 NarrowedFromUnion<'A'>
是联盟的一个分支,NarrowedFromUnion<'B'>
是另一个分支,它们一起组成 Union
.
现在我们可以定义theMap
的类型:
const theMap: {[K in Union['type']]: (u: NarrowedFromUnion<K>) => string } = {
A: ({ a }) => 'this is a: ' + a,
B: ({ b }) => 'this is b: ' + b
};
它是一个 mapped type,其中包含 Union
中每种类型的 属性,它是从 该特定类型 到 string
。这是详尽无遗的:如果您遗漏 A
或 B
之一,或者将 B
函数放在 A
属性 上,编译器会报错。
这意味着我们可以省略 {a}
和 {b}
上的显式注释,因为 theMap
的类型现在正在强制执行此约束。这很好,因为代码中的显式注解并不安全;你可以切换注释而不被编译器警告,因为它只知道输入是 Union
。 (这种函数参数的不合理类型缩小称为 bivariance,它在 TypeScript 中喜忧参半。)
现在我们应该让 handle
在传入的 Union
参数的 type
中成为泛型:
TS2.7+ 更新,以下函数需要类型断言,因为不支持我一直调用的内容 correlated types。
const handle = <K extends Union['type']>(u: NarrowedFromUnion<K>): string =>
(theMap[u.type] as (_: typeof u) => string)(u);
好的,很多。希望能帮助到你。祝你好运!
我希望 TypeScript 在像这样映射联合时强制执行穷举:
type Union =
{ type: 'A', a: string } |
{ type: 'B', b: number }
联合事件处理器:
const handle = (u: Union): string =>
theMap[u.type](u);
如果我们能以某种方式从 TypeScript 获得详尽检查,那就太好了:
const theMap: { [a: string]: (u: Union) => string } = {
A: ({a}: { type: 'A', a: string }) => 'this is a: ' + a,
B: ({b}: { type: 'B', b: number }) => 'this is b: ' + b
};
TS2.8+更新
因为conditional types were released, the manipulation needed to strongly type theMap
has gotten a lot easier. Here we will use Extract<U, X>
采用联合类型U
和return只有那些可分配给X
的成分:
type Union = { type: "A"; a: string } | { type: "B"; b: number };
const theMap: {
[K in Union["type"]]: (u: Extract<Union, { type: K }>) => string
} = {
A: ({ a }) => "this is a: " + a,
B: ({ b }) => "this is b: " + b
};
超级简单!不幸的是,自 TS2.7 左右以来,编译器不再允许您调用 theMap(u.type)(u)
。函数 theMap(u.type)
与值 u
相关,但编译器看不到。相反,它将 theMap(u.type)
和 u
视为独立的联合类型,并且不会让您在没有类型断言的情况下调用另一个:
const handle = (u: Union): string =>
(theMap[u.type] as (v: Union) => string)(u); // need this assertion
或不手动遍历可能的联合值:
const handle = (u: Union): string =>
u.type === "A" ? theMap[u.type](u) : theMap[u.type](u); // redundant
我通常建议人们为此使用断言。
我有一个关于此类 correlated types 的未决问题,但我不知道是否会支持它。无论如何,再次祝你好运!
TS2.7及以下答案:
鉴于定义的类型 Union
,很难(或者可能不可能)哄骗 TypeScript 为您提供一种表达详尽性检查的方法(theMap
只包含一个用于每个成分的处理程序联合的类型)和稳健性约束(theMap
中的每个处理程序都是针对联合的 特定 组成类型的。
但是,可以根据更一般的类型来定义 Union
,您也可以从中表达上述约束。先来看比较通用的类型:
type BaseTypes = {
A: { a: string };
B: { b: number };
}
这里,BaseTypes
是从原来的Union
的type
属性到去掉type
的构成类型的映射。由此,Union
等价于({type: 'A'} & BaseTypes['A']) | ({type: 'B'} & BaseTypes['B'])
。
让我们在类型映射上定义一些操作,例如 BaseTypes
:
type DiscriminatedType<M, K extends keyof M> = { type: K } & M[K];
type DiscriminatedTypes<M> = {[K in keyof M]: DiscriminatedType<M, K>};
type DiscriminatedUnion<M, V=DiscriminatedTypes<M>> = V[keyof V];
您可以验证 Union
等同于 DiscriminatedUnion<BaseTypes>
:
type Union = DiscriminatedUnion<BaseTypes>
此外,定义 NarrowedFromUnion
:
type NarrowedFromUnion<K extends Union['type']> = DiscriminatedType<BaseTypes, K>
它采用一个键 K
并生成与该 type
的并集的组成部分。所以 NarrowedFromUnion<'A'>
是联盟的一个分支,NarrowedFromUnion<'B'>
是另一个分支,它们一起组成 Union
.
现在我们可以定义theMap
的类型:
const theMap: {[K in Union['type']]: (u: NarrowedFromUnion<K>) => string } = {
A: ({ a }) => 'this is a: ' + a,
B: ({ b }) => 'this is b: ' + b
};
它是一个 mapped type,其中包含 Union
中每种类型的 属性,它是从 该特定类型 到 string
。这是详尽无遗的:如果您遗漏 A
或 B
之一,或者将 B
函数放在 A
属性 上,编译器会报错。
这意味着我们可以省略 {a}
和 {b}
上的显式注释,因为 theMap
的类型现在正在强制执行此约束。这很好,因为代码中的显式注解并不安全;你可以切换注释而不被编译器警告,因为它只知道输入是 Union
。 (这种函数参数的不合理类型缩小称为 bivariance,它在 TypeScript 中喜忧参半。)
现在我们应该让 handle
在传入的 Union
参数的 type
中成为泛型:
TS2.7+ 更新,以下函数需要类型断言,因为不支持我一直调用的内容 correlated types。
const handle = <K extends Union['type']>(u: NarrowedFromUnion<K>): string =>
(theMap[u.type] as (_: typeof u) => string)(u);
好的,很多。希望能帮助到你。祝你好运!