Typescript:检查类型是否为联合类型

25

有没有可能检查给定的类型是否是联合类型?

type IsUnion<T> = ???

为什么我需要这个:在我的代码中,我只有一种情况可以使用联合类型。我使用了一个分配式条件类型来处理它。然而,对于那些看到这段代码的人来说,为什么要使用DCT可能并不明显。因此,我希望它像这样明确:IsUnion<T> extends true ? T extends Foo ...

我尝试过使用UnionToIntersection,但没有结果。我也想出了这个:

type IsUnion<T, U extends T = T> =
    T extends any ?
    (U extends T ? false : true)
    : never

对于非联合类型,它返回false,但出于某种原因,对于联合类型,它返回boolean... 我不知道为什么。我还尝试从T中推断U,但没有成功。

P.S. 对于某些人来说,我的用例可能看起来不完美/正确/好,但无论如何,标题中的问题已经出现了,我想知道是否有可能解决(我感觉可以,但自己很难弄清楚)。

4个回答

28

看起来我已经自己找到了答案!

这是类型(感谢Titian Cernicova-Dragomir的简化!):

type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true

type Foo = IsUnion<'abc' | 'def'> // true
type Bar = IsUnion<'abc'> // false

再次使用jcalz的UnionToIntersection工具非常方便!

这个原理基于一个事实,即联合类型A | B不能扩展交叉类型A & B

Playground

更新。我太傻了,没有将我的问题类型转换为这个类型,但这个也很好用:

type IsUnion<T, U extends T = T> =
    (T extends any ?
    (U extends T ? false : true)
        : never) extends false ? false : true

它将联合体T分配给组成部分,也包括T,然后检查联合体U是否扩展了组成部分T。如果是,则不是联合体(但我仍然不知道为什么不添加extends false ? false : true就无法正常工作,即为什么前面的部分对于联合体返回boolean)。

1
条件 [T] extends [UnionToIntersection<T>] 不足以满足吗? - Titian Cernicova-Dragomir
@TitianCernicova-Dragomir 哦,看起来确实是这样的 :D - Nurbol Alpysbayev
1
我可能很蠢。我看了这个答案,但在我的脑海中,我是从头开始想出来的。现在我意识到它几乎完全相同。但是回答为什么有额外的条件:U被分配了。所以你在这里得到了基本上一个嵌套的for循环,而一些成员不扩展其他成员包括true在输出中。当与自身进行比较时,成员将评估为false。因此,对于联合,结果是true | false,也就是boolean - Keith Layne
为什么[T] extends [UnionToIntersection<T>]的两侧都需要加上括号?T extends UnionToIntersection<T>不行吗? - 3dGrabber
1
@3dGrabber 因为如果我们省略方括号,那么我们将会有一个naked类型参数T,它将被Typescript“分发”。更多信息请参见:https://www.typescriptlang.org/docs/handbook/advanced-types.html#distributive-conditional-types https://dev59.com/Z1UK5IYBdhLWcg3wjwiY - Nurbol Alpysbayev
遗憾的是,这对于可选联合类型如 string | undefined 并不适用。 - rcbevans

4

注意: 这个答案是为了一个明确不想使用 UnionToIntersection 的情况而准备的。那个版本简单易懂,如果你对 U2I 没有任何疑虑,那就选择它吧。

我刚刚再次查看了这个问题,并在 @Gerrit0 的帮助下得出了以下结论:

// Note: Don't pass U explicitly or this will break.  If you want, add a helper
// type to avoid that.
type IsUnion<T, U extends T = T> = 
  T extends unknown ? [U] extends [T] ? false : true : false;

type Test = IsUnion<1 | 2> // true
type Test2 = IsUnion<1> // false
type Test3 = IsUnion<never> // false

看起来可能还可以进一步简化,我对此感到非常满意。这里的诀窍是分发 T 而不是 U,以便您可以比较它们。因此,对于type X = 1 | 2,您最终会检查[1 | 2] extends [1],这是错误的,因此此类型总体上为true。如果T = never,我们也会解析为false(感谢 Gerrit)。

如果该类型不是联合类型,则TU相同,所以此类型解析为false

注意事项

有些情况下,这种方法无法正常工作。任何具有可分配给另一个成员的成员的联合将解析为boolean,因为T的分发。可能最简单的例子是当{}在联合中时,因为几乎所有内容(甚至原始数据)都可以赋值给它。在包含两个对象类型的联合中,您还将看到其中一个是另一个的子类型的情况,即{ x: 1 } | { x: 1, y: 2 }

解决方法

  1. 使用第三个 extends 子句(例如 Nurbol 的答案)
(...) extends false ? false : true;
  1. 在错误情况下使用never
T extends unknown ? [U] extends [T] ? never : true : never;
  1. 在调用处反转extends
true extends IsUnion<T> ? Foo : Bar;
  • 由于在调用此函数时可能需要使用条件类型,因此请将其包装起来:
  • type IfUnion<T, Yes, No> = true extends IsUnion<T> ? Yes : No;
    

    根据您的需求,这种类型有很多其他的变化。一个想法是在正例情况下使用unknown。然后您可以执行T&IsUnion<T>。或者您只能使用T 并将其命名为AssertUnion,如果它不是联合,则整个类型变为never。天空是无限的。

    感谢Gitter上的@Gerrit0和@AnyhowStep发现我的错误并提供解决方法的反馈。


    我很困惑 T extends unknown 是什么意思?一旦我删除了这个判断,它就失败了。 - RickShao
    1
    @RickShao 请参阅 https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types - 这会触发条件的“分配” - 类似于类型联合上的 map。现在我通常会写类似 T extends T ? ... 的东西 - 它更短,也许更清晰。当大多数人第一次遇到分配式条件类型时,他们会感到惊讶。 - Keith Layne

    3

    这里其他人给出的两个答案都可能会导致意料之外的结果,原因如下:

    type Foo = IsUnion<'a' | string> // false!?
    type Bar = IsUnion<boolean>      // true!?
    

    这是因为Typescript折叠类型的方式不同:

    type a = 'a' | string // string
    

    所以,除非能够控制提供的联合类型不会折叠,否则目前不可能使用IsUnion,而且这可能是一个坏主意,因为它可能导致意想不到和令人惊讶的结果。

    1
    IsUnion<'a' | string>false 是正确的; "a" | string 只是写 string 的冗长方式,与 IsUnion 完全无关:type X = "a" | string; 使 X 成为 string 的别名。IsUnion<boolean>true 最初令人惊讶,但只是最初。booleantrue | false,这是一个联合类型。 - T.J. Crowder

    0
    TypeScript 5 的解决方案:
    type IsUnion<T> = (
      [T, never] extends [infer U, never]
        ? U extends unknown ? [T, keyof U] extends [U | boolean, keyof T] ? false : true
        : never
        : never
    ) extends false ? false : true;
    

    适用于布尔值和空对象。

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