如何在 TypeScript 中重用递归联合类型的公共部分?

Posted

技术标签:

【中文标题】如何在 TypeScript 中重用递归联合类型的公共部分?【英文标题】:How to reuse common parts of recursive union types in TypeScript? 【发布时间】:2020-05-20 06:36:47 【问题描述】:

TLDR

这可行(Playground):

type SimpleExpression = number | string | AddOperator<SimpleExpression> | PrintOperator<SimpleExpression>;
type ExtendedExpression = number | string | AddOperator<ExtendedExpression> | PrintOperator<ExtendedExpression> | PowerOperator<ExtendedExpression>;

但是提取公共子类型不起作用(Playground):

type CommonExpression<E> = number | string | AddOperator<E> | PrintOperator<E>;
type SimpleExpression = CommonExpression<SimpleExpression>;
type ExtendedExpression = CommonExpression<ExtendedExpression> | PowerOperator<ExtendedExpression>;

有什么办法吗?

详细说明

最近 TypeScript (3.7) 获得了对递归类型的扩展支持,但并非一切皆有可能。 *** 上有很多解释递归类型的问题,但有一种有用的模式我找不到解决方案。

一个起点是一个表达式树,其中每个运算符都有一个子表达式。这有效:

type Expression = number | string | AddOperator | PrintOperator;
interface AddOperator 
  'first': Expression;
  'second': Expression;

interface PrintOperator 
  'value': Expression;

现在我们想让它更通用,这样我们就有了 SimpleExpression(只有 addprint),以及还支持 power 运算符的 ExtendedExpression。 我们可以做到这一点,并且有效 (Playground):

interface AddOperator<E> 
  'first': E;
  'second': E;

interface PrintOperator<E> 
  'value': E;

interface PowerOperator<E> 
  'value': E;
  'exp': E;


type SimpleExpression = number | string | AddOperator<SimpleExpression> | PrintOperator<SimpleExpression>;

type ExtendedExpression = number | string | AddOperator<ExtendedExpression> | PrintOperator<ExtendedExpression> | PowerOperator<ExtendedExpression>;

但是现在如果我们想使用通用联合类型来分解公共部分,我们将失败 (Playground):

type CommonExpression<E> = number | string | AddOperator<E> | PrintOperator<E>;
type SimpleExpression = CommonExpression<SimpleExpression>;
type ExtendedExpression = CommonExpression<ExtendedExpression> | PowerOperator<ExtendedExpression>;

错误是:

Error: Type alias 'SimpleExpression' circularly references itself.
Error: Type alias 'ExtendedExpression' circularly references itself.

所以问题是:有没有办法定义两种不同的表达式类型,它们共享共同的核心?

关于使用场景的一些背景知识:我们想提供 SimpleExpression 在库中,但允许客户端代码定义 额外的运算符,并为它们注册处理程序。我们想要用户 能够轻松定义他的ExtendedExpression 类型,而无需过多输入。

【问题讨论】:

你能告诉我为什么它是递归的吗,看起来很奇怪我有像value这样的东西,而在value里面有value等等在如此深的嵌套中有什么意义可能性? 表达式本质上是递归的:你可以添加加法的结果(a+b) + (c+d),然后添加两个类似( (a+b) + (c+d) ) + ( (e+f) + (g+h) )的东西,等等。需要任意嵌套。 【参考方案1】:

当你使用

type SimpleExpression = number | string | AddOperator<SimpleExpression> | PrintOperator<SimpleExpression>;
type ExtendedExpression = number | string | AddOperator<ExtendedExpression> | PrintOperator<ExtendedExpression> | PowerOperator<ExtendedExpression>;

至少有一条路径应该停止递归。对于const simple: SimpleExpression = value: value: 5 ; 的情况,其类型为PrintOperator&lt;PrintOperator&lt;number&gt;&gt;


但是如果你使用

type CommonExpression<E> = number | string | AddOperator<E> | PrintOperator<E>;
type SimpleExpression = CommonExpression<SimpleExpression>;
type ExtendedExpression = CommonExpression<ExtendedExpression> | PowerOperator<ExtendedExpression>;

递归永远不会停止。事实上,递归本身就发生在类型声明本身中。 SimpleExpression 将是 CommonExpression&lt;SimpleExpression&gt; 类型,但它会扩展为 CommonExpression&lt;CommonExpression&lt;SimpleExpression&gt;&gt;,然后是 CommonExpression&lt;CommonExpression&lt;CommonExpression&lt;SimpleExpression&gt;&gt;&gt;,以此类推,形成无限递归。这就是 TypeScript 不接受这种类型设置的原因。


有趣的是,

type SimpleExpression = AddOperator<SimpleExpression> | PrintOperator<SimpleExpression>;

仍然被接受,但没有变量会满足这种类型。

【讨论】:

感谢您的回答。递归不是“无限的”,因为CommonExpression有条件地 扩展为依赖于它的参数的东西。因此,递归可以毫无问题地停止。 TypeScript 的类型分析是以认为无限递归的方式编写的这一事实在这里没有争议,而是我想解决的一个问题。

以上是关于如何在 TypeScript 中重用递归联合类型的公共部分?的主要内容,如果未能解决你的问题,请参考以下文章

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

F#如何在递归可区分联合中指定类型限制

如何从 Typescript 中的常量定义字符串文字联合类型

在 Typescript 中命名联合类型

在 TypeScript 中重载联合类型

TypeScript系列教程16TypeScript 联合类型