类型化对象并集的详尽图 [英] Exhaustive map over a union of typed objects

查看:50
本文介绍了类型化对象并集的详尽图的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我希望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是从原始Uniontype属性到删除了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的函数.这是详尽无遗的:如果省略AB之一,或将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:

针对TS2.7 +的更新,由于缺少对我一直称呼的

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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆