类型化对象并集的详尽图 [英] Exhaustive map over a union of typed objects
问题描述
我希望TypeScript在通过这样的联合进行映射时强制执行详尽性:
I want TypeScript to enforce exhaustiveness when mapping over a union like this:
type Union =
{ type: 'A', a: string } |
{ type: 'B', b: number }
Union事件处理程序:
The Union event handler:
const handle = (u: Union): string =>
theMap[u.type](u);
如果在这里我们能够以某种方式从TypeScript中获得详尽的检查,那就太好了
It'd be great if here we could somehow get exhaustiveness check from 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 +的更新
由于条件类型被释放,强类型输入theMap
所需的操作变得容易得多.在这里,我们将使用 U
并仅返回可分配给X
的那些成分:
UPDATE FOR TS2.8+
Since conditional types were released, the manipulation needed to strongly type theMap
has gotten a lot easier. Here we will use Extract<U, X>
to take a union type U
and return only those constituents that are assignable to 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
视为独立的联合类型,并且在没有类型声明的情况下不允许您彼此调用:
Super simple! Unfortunately, the compiler no longer allows you to call theMap(u.type)(u)
since TS2.7 or so. The function theMap(u.type)
is correlated to the value u
, but the compiler doesn't see that. Instead it sees theMap(u.type)
and u
as independent union types and won't let you call one on the other without a type assertion:
const handle = (u: Union): string =>
(theMap[u.type] as (v: Union) => string)(u); // need this assertion
,或者无需手动遍历可能的并集值:
or without manually walking through the possible union values:
const handle = (u: Union): string =>
u.type === "A" ? theMap[u.type](u) : theMap[u.type](u); // redundant
我通常建议人们为此使用断言.
I've generally been recommending people use assertions for this.
关于这样的相关类型,我遇到了一个公开问题但我不知道是否会对此提供支持.无论如何,再次祝你好运!
I've got an open issue about such correlated types but I don't know if there will ever be support for it. Anyway, good luck again!
TS2.7及以下版本的答案:
TS2.7 and below answer:
鉴于类型Union
的定义,很难(或不可能)哄骗TypeScript为您提供一种方式来表达穷举性检查(theMap
对于联合的每种组成类型均只包含一个处理程序)和健全性约束(theMap
中的每个处理程序都针对联合的特定组成类型).
Given the type Union
as defined, it's hard (or maybe impossible) to coax TypeScript into giving you a way to express both the exhaustiveness check (theMap
contains exactly one handler for each constituent type of the union) and the soundness constraint (each handler in theMap
is for a specific constituent type of the union).
但是,可以用更通用的类型来定义Union
,从中您还可以表达上述约束.首先让我们看一下更通用的类型:
However, it's possible to define Union
in terms of a more general type, from which you can also express the above constraints. Let's look at the more general type first:
type BaseTypes = {
A: { a: string };
B: { b: number };
}
在这里,BaseTypes
是从原始Union
的type
属性到删除了type
的组成类型的映射.由此,Union
等同于({type: 'A'} & BaseTypes['A']) | ({type: 'B'} & BaseTypes['B'])
.
Here, BaseTypes
is a mapping from the type
property of the original Union
to the constituent types with type
removed from them. From this, Union
is equivalent to ({type: 'A'} & BaseTypes['A']) | ({type: 'B'} & BaseTypes['B'])
.
让我们在类型映射上定义一些操作,例如BaseTypes
:
Let's define some operations on type maps like 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
.
which takes a key K
and produces the constituent of the union with that type
. So NarrowedFromUnion<'A'>
is one leg of the union and NarrowedFromUnion<'B'>
is the other, and together they make up Union
.
现在我们可以定义theMap
的类型:
Now we can define the type of theMap
:
const theMap: {[K in Union['type']]: (u: NarrowedFromUnion<K>) => string } = {
A: ({ a }) => 'this is a: ' + a,
B: ({ b }) => 'this is b: ' + b
};
这是一个包含一个属性的映射类型对于Union
中的每种类型,这是从特定类型到string
的函数.这是详尽无遗的:如果省略A
或B
之一,或将B
函数放在A
属性上,则编译器会抱怨.
It's a mapped type containing one property for each type in Union
, which is a function from that specific type to a string
. This is exhaustive: if you leave out one of A
or B
, or put the B
function on the A
property, the compiler will complain.
这意味着我们可以省略{a}
和{b}
上的显式注释,因为theMap
的类型现在强制执行此约束.很好,因为代码中的显式注释并不十分安全;您可能已经更改了注解,而没有被编译器警告,因为它所知道的只是输入是Union
. (这种对函数参数的不合理的类型缩小称为 bivariance ,这是TypeScript中的好运.)
That means we are able to omit the explicit annotation on {a}
and {b}
, since the type of theMap
is now enforcing this constraint. That's good, because the explicit annotation from your code was not really safe; you could have switched the annotations around and not been warned by the compiler, because all it knew was that the input was a Union
. (This kind of unsound type narrowing of function parameters is called bivariance and it's a mixed blessing in TypeScript.)
现在,我们应该在传入的Union
参数的type
中使handle
通用:
Now we should make handle
be generic in the type
of the Union
parameter passed in:
Update for TS2.7+, the following function needs a type assertion due to lack of support for what I've been calling correlated types.
const handle = <K extends Union['type']>(u: NarrowedFromUnion<K>): string =>
(theMap[u.type] as (_: typeof u) => string)(u);
好的,很多.希望能帮助到你.祝你好运!
Okay, that was a lot. Hope it helps. Good luck!
这篇关于类型化对象并集的详尽图的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!