挑战

实现一个类型IsUnion,它接受输入类型T,并返回T是否解析为联合类型。

例如:

type case1 = IsUnion<string>; // false
type case2 = IsUnion<string | number>; // true
type case3 = IsUnion<[string | number]>; // false

解答

当我看到这样的挑战时,我总是感到沮丧。因为没有通用的解决方案可以用来实现这样的类 型,也没有可以使用的内置类型或intrinsic

这样我们就必须创造性地结合我们所掌握的知识,我们首先思考联合类型及其代表的含义。

当你指定一个普通类型,例如string,它将永远是字符串。但是,当你指定一个联合类型 时,例如string | number,你可以从中取得一组潜在的值。

普通类型并不能表示一组值,但联合类型可以。在普通类型上进行分布式迭代是没有意义 的,但对于联合来说是有意义的。

这就是我们如何检测一个类型是否为联合类型所依赖的关键区别。当对类型T(不是联合 类型)进行分布式迭代时,它没有任何改变。但是,如果T是联合类型的话,它的变化会 很大。

TypeScript 有一个很棒的类型特性——分布式条件类型。当你编写这样的构 造:T extends string ? true : false,其中T是一个适用于分布式条件类型的联合类 型。粗略地说,这看起来就像将条件类型应用于联合类型中的每个元素。

type IsString<T> = T extends string ? true : false;

// For example, we provide type T = string | number
// It is the same as this
type IsStringDistributive = string extends string
  ? true
  : false | number extends string
  ? true
  : false;

你明白我的意思了吧?如果类型T是一个联合类型,通过使用分布式条件类型,我们可以分 离该联合类型并将其与输入类型T进行比较。如果T不是一个联合类型,那么在这种情况 下,二者是一样的。但是,当它是一个联合类型时,它们二者的结果就不一样了,因 为string不是由string | number 扩展而来,当然number也不是。

让我们动手实现这个类型吧!首先,我们将复制输入类型 T,这样就可以保留 m 没有经过任 何修改的输入类型T,稍后我们将对它们进行比较。

type IsUnion<T, C = T> = never;

通过应用条件类型,我们得到了分布式语义。在条件类型的 true 分支中,我们将获取联合 类型中的每一项。

type IsUnion<T, C = T> = T extends C ? never : never;

现在是最重要的部分——将每一项与原来的输入类型T进行比较。在没有应用分布式迭代的 情况下(T不是联合类型),[C][T]是相同的,因此为false。否则,T是一个联 合类型,因此将应用分配式迭代,将联合类型中的单项与联合类型本身进行比较,因此 为true

type IsUnion<T, C = T> = T extends C ? ([C] extends [T] ? false : true) : never;

齐活儿!

为了更清楚地阐述,接下向你展示 [C][T] 在分布式条件类型中true分支中代表 什么。

当我们传入的不是联合类型时,例如string,它们包含相同的类型。意思是,它不是联合 类型,因此返回false

[T] = [string][C] = [string];

但是,如果我们传入一个联合类型,例如string | number,它们包含不同的类型。我们 的副本C保存了一个内部有联合类型的元组类型,而我们的T保存了一个内部元素是元组 类型的联合类(这要归功于分布条件类型),因此它是一个联合类型。

[T] = [string] | [number]
[C] = [string | number]

参考