类型化对象联合上的详尽映射

Posted

技术标签:

【中文标题】类型化对象联合上的详尽映射【英文标题】:Exhaustive map over a union of typed objects 【发布时间】:2018-03-20 08:44:06 【问题描述】:

我希望 TypeScript 在像这样映射联合时强制穷举:

type Union = 
   type: 'A', a: string  |
   type: 'B', b: number 

Union 事件处理程序:

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
;

【问题讨论】:

【参考方案1】:

TS2.8+ 更新

自从conditional types 发布以来,强类型theMap 所需的操作变得容易得多。在这里,我们将使用 Extract<U, X> 获取联合类型 U 并仅返回可分配给 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 是从原始Uniontype 属性到删除了type 的构成类型的映射。由此,Union 等价于(type: 'A' &amp; BaseTypes['A']) | (type: 'B' &amp; 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&lt;BaseTypes&gt;

type Union = DiscriminatedUnion<BaseTypes>

此外,定义NarrowedFromUnion 也很有帮助:

type NarrowedFromUnion<K extends Union['type']> = DiscriminatedType<BaseTypes, K>

它接受一个键 K 并生成与该 type 的联合的组成部分。所以NarrowedFromUnion&lt;'A'&gt; 是联盟的一个分支,NarrowedFromUnion&lt;'B'&gt; 是另一个分支,它们共同构成了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 的函数。这很详尽:如果您遗漏了AB 之一,或者将B 函数放在A 属性上,编译器会报错。

这意味着我们可以省略 ab 上的显式注释,因为 theMap 的类型现在强制执行此约束。这很好,因为您的代码中的显式注释并不安全;你可以切换注释而不被编译器警告,因为它只知道输入是Union。 (这种不合理的函数参数类型缩小称为bivariance,在 TypeScript 中是喜忧参半。)

现在我们应该在传入的Union 参数的type 中使handle 通用:

TS2.7+ 更新,以下函数需要类型断言,因为不支持我一直调用的 correlated types。

const handle = <K extends Union['type']>(u: NarrowedFromUnion<K>): string =>
  (theMap[u.type] as (_: typeof u) => string)(u);

好的,很多。希望能帮助到你。祝你好运!

【讨论】:

嘿!已经有一段时间。所以我正在重新审视这个,因为我需要在一个严重依赖这种模式的项目上更新打字稿。不过最新的TSdoesn't work with this。想把事情擦亮吗? 这种theMap[u.type](u) 模式从大约 TS2.7 开始就需要类型断言(例如,(theMap[u.type] as (_: typeof u) =&gt; string)(u))。由于缺乏对我一直称为"correlated types" 的支持,这是一个痛点。 好的,我更新了新版本 TS 的答案。 用你的答案教育自己总是很快乐。等待你关于高级东西的书。好吧,所以,为了隐藏这个异常,我努力将整个事情抽象成一个函数。我想说我成功了,但我希望你能够改进它。因此,我将其发布为another question here。

以上是关于类型化对象联合上的详尽映射的主要内容,如果未能解决你的问题,请参考以下文章

如何将联合类型指定为对象键 Typescript

“联合类型值到字符串的映射”的打字稿类型?

Hibernate 中 联合主键映射 组合关系映射 大对象映射(或者说文本大对象,二进制数据大对象)

TypeScript:从字符串数组定义联合类型

带有嵌套对象的联合上的 LINQ to Entities 空引用

打字稿中具有联合类型键的松散类型对象