TypeScript全面指南(三)
类型系统层级
所谓类型系统层级,指的是 TypeScript 中所有类型的兼容关系,从最上面一层的 any
类型,到最底层的 never
类型。类似 Java 中多态的概念,子类的实例能够赋给父类变量。在 TypeScript 中,如果 a = b
成立,意味着 <b的类型> extends <a的类型>
成立,即 b 类型是 a 类型的子类型,也可以说,在类型系统层级中,a 类型的层级比 b 类型的层级要高。
在不考虑 Object
类型,以及 String
、 Boolean
、 Number
这些包装类型的情况下,类型系统层级大概如下:
any / unknown
> string / number / boolean
> '' / 1 / true
> never
形如 A extends B ? val1 : val2
的结构,称为条件类型,常用于给类型别名赋值:
type t1 = never extends 'reimu747' ? 1 : 0; // 1
// 联合类型,只需要符合其中一个类型即可
type t2 = 'reimu747' extends 'reimu747' | false | 18 ? 1 : 0; // 1
type t3 = 'reimu747' | '' extends string ? 1 : 0; // 1
type t4 = string extends any ? 1 : 0; // 1
type t5 = string extends unknown ? 1 : 0; // 1
一些特殊情况
多个联合类型成员的情况,判断左边是否是右边的子集:
// 判断左边是否是右边的子集 type t6 = 1 | 2 | 3 extends 1 | 2 | 3 | 4 ? 1 : 2; // 1 type t7 = 2 | 4 extends 1 | 2 | 3 | 4 ? 1 : 2; // 1 type t8 = 1 | 2 | 5 extends 1 | 2 | 3 | 4 ? 1 : 2; // 2 type t9 = 1 | 5 extends 1 | 2 | 3 | 4 ? 1 : 2; // 2
数组和元组
// 类似联合类型的判断方法,左边为右边的子集 type t10 = [number, number] extends number[] ? 1 : 2; // 1 type t11 = [number, string] extends number[] ? 1 : 2; // 2 type t12 = [number, string] extends (number | string)[] ? 1 : 2; // 1 // []代表长度为0的元组,相当于never[] type t13 = [] extends number[] ? 1 : 2; // 1 type t14 = [] extends unknown[] ? 1 : 2; // 1 // 不好判断时,可以想想左边类型的值,能不能赋给右边类型的变量? type t15 = any[] extends number[] ? 1 : 2; // 1 type t16 = unknown[] extends number[] ? 1 : 2; // 2 type t17 = never[] extends number[] ? 1 : 2; // 1
any、unknown
很明显,任何类型都是
any/unknown
的子类型:type t18 = [] extends any ? 1 : 2; // 1 type t19 = 123 extends unknown ? 1 : 2; // 1
但是当
any
作为extends
左边的部分时,规则有点不同。简单来说,这时的类型,为冒号两边类型的联合类型:type t20 = any extends '' ? 1 : 2; // 1 | 2 type t21 = any extends string ? 1 : 2; // 1 | 2 type t22 = any extends {} ? 1 : 2; // 1 | 2 type t23 = any extends never ? 1 : 2; // 1 | 2
当我们使用
any extends
时,它包含了条件成立的一部分,以及条件不成立的另一部分。从实现上说,在 TypeScript 内部代码的条件类型处理中,如果接受判断的是 any,那么会直接返回条件类型结果组成的联合类型。any
和unknown
互相比较也是成立的:type t24 = any extends unknown ? 1 : 2; // 1 type t25 = unknown extends any ? 1 : 2; // 1
never
never
在作为泛型参数时,会跳过判断:// 直接使用,仍然会进行判断 type tn1 = never extends string ? 1 : 2; // 1 // 通过泛型参数传入,会跳过判断 type tn2<T> = T extends string ? 1 : 2; type res1 = tn2<never>; // never // 如果判断条件是 never,还是仅在作为泛型参数时才跳过判断 type tn3 = never extends never ? 1 : 2; // 1 type tn4<T> = T extends never ? 1 : 2; type res2 = tn4<never>; // never
左右两边相同时,也算作符合条件
type t26 = any extends any ? 1 : 2; // 1 type t27 = unknown extends unknown ? 1 : 2; // 1 type t28 = never extends never ? 1 : 2; // 1 type t29 = [] extends [] ? 1 : 2; // 1 type t30 = '' extends '' ? 1 : 2; // 1 type t31 = false extends false ? 1 : 2; // 1
infer 关键字
用在条件类型中,表示待推断的类型。例子如下:
// 返回交换长度为2的元组后的类型
// 此时我们并不知道A,B的类型信息,因此用infer表示其类型待推断
type Swap<T extends any[]> = T extends [infer A, infer B] ? [B, A] : T;
type SwapResult1 = Swap<[1, 2]>; // [2, 1]
type SwapResult2 = Swap<[1, 2, 3]>; // 仍是 [1, 2, 3]
// 提取首尾元素
type ExtractStartAndEnd<T extends any[]> = T extends [infer Start, ...any[], infer End] ? [Start, End] : T;
// 调换首尾元素
type SwapStartAndEnd<T extends any[]> = T extends [infer Start, ...infer Left, infer End] ? [End, ...Left, Start] : T;
// 调换开头两个
type SwapFirstTwo<T extends any[]> = T extends [infer Start1, infer Start2, ...infer Left]
? [Start2, Start1, ...Left]
: T;
分布式条件类型
TypeScript 在解析条件类型时,有一条特殊的规则,称之为分布式条件类型。官方解释为:对于属于裸类型参数的检查类型,条件类型会在实例化时期自动分发到联合类型上。具体来说,满足以下的条件,就会形成分布式条件类型:
- 类型参数是一个联合类型;
- 该类型参数要通过泛型参数的方式传入,而不能直接进行条件类型判断;
- 条件类型中的泛型参数不能被包裹。
形成分布式条件类型后,效果是将这个联合类型拆开来,每个分支分别进行一次条件类型判断,再将最后的结果合并起来。代码例子如下:
type Condition<T> = T extends 1 | 2 | 3 ? T : never;
// 形成分布式条件类型,结果为 1 | 2 | 3, 而不是never
type Res1 = Condition<1 | 2 | 3 | 4 | 5>;
// 不满足上述条件2,结果为never
type Res2 = 1 | 2 | 3 | 4 | 5 extends 1 | 2 | 3 ? 1 | 2 | 3 | 4 | 5 : never;
type Naked<T> = T extends boolean ? 'Y' : 'N';
type Wrapped<T> = [T] extends [boolean] ? 'Y' : 'N';
// 形成分布式条件类型,结果为 "N" | "Y", 而不是 "N"
type Res3 = Naked<number | boolean>;
// 不满足上述条件3,结果为 "N"
type Res4 = Wrapped<number | boolean>;
分布式条件类型提供了更强大的处理联合类型(union)的能力。在 TypeScript 内置工具类型中,交集和差集就是基于分布式条件类型实现的:
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;