【问题标题】:Is there a way to prevent union types in TypeScript?有没有办法防止 TypeScript 中的联合类型?
【发布时间】:2020-11-18 22:42:22
【问题描述】:

我是条件类型的新手,所以我尝试了最明显的静态方式,没有成功:

type NoUnion<Key> =
  Key extends 'a' ? 'a' :
  Key extends 'b' ? 'b' :
  never;

type B = NoUnion<'a'|'b'>;

B 类型仍然是联合。有人请教我吗?

这是playground

【问题讨论】:

  • 你想达到什么目的?
  • @mast3rd3mon 解决this one
  • 你希望B 不是一个联合体?
  • @AsadSaeeduddin 在示例中为never。在使用中,'a''b'
  • 类型参数中的联合类型传递了NoUnion 中的两个条件类型,所以你最终得到a|b。当第一个条件通过时,条件类型不会停止。

标签: typescript functional-programming


【解决方案1】:

我不确定这是什么用例,但如果传递的类型是联合类型,我们可以将 NoUnion 强制为 never

由于其他提到的条件类型分布在联合上,这称为distributive conditional types

检查类型是裸类型参数的条件类型称为分布式条件类型。分布式条件类型在实例化期间自动分布在联合类型上。例如,一个 T 的实例扩展了 U ? X : Y 类型参数 A |乙| C for T 被解析为 (A extends U ? X : Y) | (B 扩展 U ? X : Y) | (C 扩展 U ? X : Y)。

这里的键是“裸类型”,如果我们将类型包装在一个元组类型中,例如条件类型将不再是可分配的。

type UnionToIntersection<U> = 
    (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never 

type NoUnion<Key> =
    // If this is a simple type UnionToIntersection<Key> will be the same type, otherwise it will an intersection of all types in the union and probably will not extend `Key`
    [Key] extends [UnionToIntersection<Key>] ? Key : never; 

type A = NoUnion<'a'|'b'>; // never
type B = NoUnion<'a'>; // a
type OtherUnion = NoUnion<string | number>; // never
type OtherType = NoUnion<number>; // number
type OtherBoolean = NoUnion<boolean>; // never since boolean is just true|false

最后一个例子是一个问题,因为boolean 被编译器视为true|falseNoUnion&lt;boolean&gt; 实际上将是never。如果没有更多关于您究竟想要实现什么的详细信息,很难知道这是否会破坏交易,但可以通过将 boolean 视为特殊情况来解决:

type NoUnion<Key> =
    [Key] extends [boolean] ? boolean :
    [Key] extends [UnionToIntersection<Key>] ? Key : never;

注意:UnionToIntersection 取自 here

【讨论】:

  • @Birowsky :)) 编写文档没有人赞成:P。这更有趣;)
  • 所以.. 我尝试对我的问题实施这个,但我得到的只是一个损坏的 TS:/ 如果你能take a look here,我将非常感激!目标是在没有断言的情况下在解析器中解决问题。 (这是一个 jsbin,因为操场链接太大,无法评论)
  • @Birowsky 调查它
  • @Birowsky 也许这可行:function resolver&lt;cmdName extends CommandName&gt;(a: Command&lt;cmdName&gt;): NoUnion&lt;cmdName&gt; extends never ? never : CommandToHandler[cmdName]['out'] { return handlers[a.kind](a); } let c : Command&lt;"a"&gt; resolver(c) // ok let cu : Command&lt;"a" | "b"&gt; resolver(cu) // returns never
  • 对不起伙计,但它不能解决Error:(26, 10) TS2349: Cannot invoke an expression whose type lacks a call signature. Type '((arg: Command&lt;"a"&gt;) =&gt; number) | ((arg: Command&lt;"b"&gt;) =&gt; string)' has no compatible call signatures. 如果你将 jsbin 中的 sn-p 粘贴到 TS 游乐场,你应该会看到这个问题。谢谢,抱歉没有说清楚。
【解决方案2】:

顺便说一句,我试图想出的“更简单”的方法如下所示:

(注意:由于microsoft/TypeScript#34504,以下内容在 v3.3 之后的 TS 中不起作用:

// type NotAUnion<T> = [T] extends [infer U] ? 
//  U extends any ? [T] extends [U] ? T : never : never : never;

相反,可以使用以下内容,因为默认值在分发之前仍然会被实例化,至少现在是这样:)

type NotAUnion<T, U = T> =
  U extends any ? [T] extends [U] ? T : never : never;

这应该可以工作(请测试一下;不知道为什么我把my answer to another question 的原始版本弄错了,但现在已经修复了)。这与UnionToIntersection 的想法相似:如果您分发它,您希望确保类型T 可以分配给T 的每个部分。一般来说,只有当T 是只有一个组成部分的联合(也称为“非联合”)时才会如此。

无论如何,@TitianCernicovaDragomir 的回答也很好。只是想把这个版本放在那里。干杯。

【讨论】:

  • 干杯,先生!顺便说一句,我认为[T] 中的括号仅用于元组。但在这里它似乎有一些其他的语义。你能澄清一下吗? (资源链接非常好)
  • 是的,它用作一个元组,以一种骇人听闻的方式来防止分发。 (参见@TitianCD 上面关于“裸型”的评论)。看看能不能找到链接。
  • This issue 是我能找到的最接近的,没有太多解释。如果T 是“裸类型参数”,则T extends U ? X : Y 分发T。这个想法是“穿上”它:[T] extends [U] 应该保持当且仅当T extends U(因为单元素元组的类型是协变的)但阻止T 的分布。在这里使用元组语法没有什么特别之处;任何协变类型都可以:{x: T} extends {x: U} 成立当且仅当T extends U 成立。但[T] 可能是最短的方法。
  • 这是一个很好的解决方案。但不知何故,它不适用于版本 > 3.3.3(我可以在操场上测试的最后一个版本)。您可以将3.3.33.5.1 进行比较。似乎推断的类型参数 U 不能再“裸”以创建分布式联合类型 - 对我来说听起来像是一个错误。
  • @ford04 感谢您指出这一点。我看到microsoft/TypeScript#34504 提到了这一点,所以我想我必须找到并更改我依赖于先前行为的任何答案(并不是说这些东西很容易搜索)
【解决方案3】:

这也有效:

type NoUnion<T, U = T> = T extends U ? [U] extends [T] ? T : never : never;

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-02-15
    • 2020-10-10
    • 1970-01-01
    • 2022-01-06
    • 1970-01-01
    • 2010-09-06
    相关资源
    最近更新 更多