有没有一种方法可以在TypeScript中防止联合类型?

11

我对条件类型还不熟悉,所以我尝试了最显然的静态方式,但没有成功:

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

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

B类型仍然是union(联合)。请有人给我讲解一下?

这里有一个playground


你想要实现什么目标? - mast3rd3mon
@mast3rd3mon 解决这个问题 - Daniel Birowsky Popeski
除了联合,您希望B是什么? - Asad Saeeduddin
在这个例子中,使用的是never。在实际使用中,则使用'a''b' - Daniel Birowsky Popeski
1
你的类型参数中的联合类型传递了 NoUnion 中的两种条件类型,因此最终得到了 a|b。当第一个条件通过时,条件类型并不会停止。 - Fenton
显示剩余3条评论
3个回答

23

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

正如其他人提到的条件类型分布在联合类型之上,这被称为分布式条件类型

检查的类型为裸类型参数的条件类型被称为分布式条件类型。在实例化期间,分布式条件类型会自动分布在联合类型上。例如,T extends U? X: Y 的实例化类型参数为 A | B | C,则该类型将被解析为 (A extends U? X: Y) | (B extends U? X: Y) | (C extends 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 | false,所以NoUnion<boolean>实际上将是never。如果没有更多关于你想要实现的具体细节,很难知道这是否是一个致命问题,但可以通过将boolean视为特殊情况来解决:
type NoUnion<Key> =
    [Key] extends [boolean] ? boolean :
    [Key] extends [UnionToIntersection<Key>] ? Key : never;

注意: UnionToIntersection 是从这里引用的。

2
@Birowsky :)) 写文档没有点赞的,这样更有趣 ;) - Titian Cernicova-Dragomir
所以..我尝试将这个应用于我的问题,但是我得到的只是一个破碎的TS :/ 如果你能在这里看一下,我会非常感激!目标是在不使用断言的情况下解决解析器中的问题。(这是一个jsbin,因为playground链接对于评论来说太大了) - Daniel Birowsky Popeski
1
@Birowsky 正在研究它。 - Titian Cernicova-Dragomir
@Birowsky 或许这个方案可行:`function resolver<cmdName extends CommandName>(a: Command<cmdName>): NoUnion<cmdName> extends never ? never : CommandToHandler[cmdName]['out'] { return handlersa.kind; } let c : Command<"a"> resolver(c) // 正常 let cu : Command<"a" | "b"> resolver(cu) // 返回 never` - Titian Cernicova-Dragomir
抱歉伙计,但这并不能解决 Error:(26, 10) TS2349: Cannot invoke an expression whose type lacks a call signature. Type '((arg: Command<"a">) => number) | ((arg: Command<"b">) => string)' has no compatible call signatures. 如果你将在 JSBin 中的代码片段粘贴到 TS Playground 中,则应该会看到此问题。感谢您的帮助,抱歉没有详细说明。 - Daniel Birowsky Popeski
显示剩余2条评论

8
顺便说一下,我试图提出的“更简单”的方法看起来像这样:
(注意:由于microsoft/TypeScript#34504,以下内容在 TypeScript v3.3 之后无法使用:)
// 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;

我希望这个可以工作(请测试一下;不确定为什么我在回答另一个问题中得到了原始版本,但现在已经修复)。这是与UnionToIntersection类似的想法:如果您分配它,则要确保类型T可分配给T的每个部分。一般来说,只有当T是具有一个组成部分的联合体时(也称为“非联合”)才成立。

无论如何,@TitianCernicovaDragomir的回答也完全可以。只是想让这个版本出现。干杯。


先生,干杯!顺便问一下,在 [T] 中的括号只用于元组,但在这里似乎有其他语义。您能澄清一下吗?(提供资源链接也可以) - Daniel Birowsky Popeski
是的,它被用作元组,但是以一种欺骗性的方式来防止分发。(请参见@TitianCD关于“naked type”的评论)。让我们看看我能否找到一个链接。 - jcalz
1
这个问题是我能找到的最接近的,但没有太多解释。如果 T 是一个“裸类型参数”,则 T extends U ? X : Y 会进行分发。其思想是要将其“包装”:仅当 T extends U (因为单元素元组在它们的类型中是协变的),才应该保持 [T] extends [U] ,但防止分发 T。这里使用元组语法并没有什么特别之处;任何协变类型都可以:{x: T} extends {x: U} 仅当 T extends U 时成立。但 [T] 可能是最简短的方法。 - jcalz
1
这是一个不错的解决方案。但不知何故,在版本 > 3.3.3(我在 playground 中测试的最后一个版本)中它不起作用。您可以将 3.3.33.5.1 进行比较。似乎无法再将推断类型参数 U 裸露出来以创建分布式联合类型 - 对我来说听起来像是一个 bug。 - ford04
1
@ford04 感谢您指出这一点。我看到microsoft/TypeScript#34504提到了这个问题,所以我想我必须找到并更改任何依赖于先前行为的答案(尽管这样的事情并不容易搜索)。 - jcalz

3

这也是有效的:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接