注:本文需要你已经对Ts交叉类型有一定的了解。
先复习下类型可赋值关系(严格模式下):
| any | unknown | object | void | undefined | null | never | |
|---|---|---|---|---|---|---|---|
| any → | ✓ | ✓ | ✓ | ✓ | ✓ | ✕ | |
| unknown → | ✓ | ✕ | ✕ | ✕ | ✕ | ✕ | |
| object → | ✓ | ✓ | ✕ | ✕ | ✕ | ✕ | |
| void → | ✓ | ✓ | ✕ | ✕ | ✕ | ✕ | |
| undefined → | ✓ | ✓ | x | ✓ | x | ✕ | |
| null → | ✓ | ✓ | x | x | x | ✕ | |
| never → | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
下面正式进入主题类型交叉运算:
运算符:“&”
运算原则:求子类型
运算符特性:
- 唯一性:A & A 等价于 A
- 满足交换律:A & B 等价于 B & A
- 满足结合律:(A & B) & C 等价于 A & (B & C)
- 父类型收敛:如果 B 是 A 的父类型,则 A & B 奖被收敛成 A 类型
常见类型交叉运算:
1、常量类型交叉
type t1 = 1 & 2; // never
type t2 = "a" & false; // never
2、基本数据类型交叉
type t1 = number & boolean; // never
type t2 = number & 2; // 2
type t3 = string & "a"; // "a"
3、对象类型交叉
type t1 = {a:number} & {a:string} // {a:never}
type t2 = {a:number} & {b:string} // {a:number,b:string},子类可扩展父类
看个例子:
type O1 = { a: number };
type O2 = { b: string };
type O3 = O1 & O2;
const o3: O3 = { a: 1, b: 'a' };
const o1: O1 = o3; // 编译通过
const o2: O2 = o3; // 编译通过
4、特殊类型交叉
4.1、never
// never是所有类型的子类型,所以和任意类型交叉计算任意类型都会被收敛,结果都是never
type t1 = number & never; // never,其它被收敛
type t2 = "a" & never; // never,其它被收敛
type t3 = void & never; // never,其它被收敛
type t4 = any & never; // never,其它被收敛
type t5 = unknown & never; // never,其它被收敛
4.2、unknown
// unknown是所有类型的父类型,所以和任意类型交叉计算unknown都会被收敛,结果都是其他类型
type t1 = number & unknown; // number, unknown都会被收敛
type t2 = "a" & unknown; // "a", unknown都会被收敛
type t3 = void & unknown; // void, unknown都会被收敛
type t4 = any & unknown; // any, unknown都会被收敛
type t5 = unknown & unknown; // unknown,unknown都会被收敛
4.3、any
// any是为了逃逸类型检查而设计的,所有类型都可以赋值给any,反过来any也可以赋值给所有类型(never除外),刨去unknown和never看any,它既可以是其他类型的父类型又可以是它们的子类型,结果就是any和这些类型交叉(&)或联合(|)计算结果都是any。
type t1 = any & 1; // any, 其它被收敛
type t2 = any & void; // any, 其它被收敛
type t3 = any & string; // any, 其它被收敛
type t4 = any & unknow; // any, 其它被收敛
type t5 = any & never; // never,any被收敛
4.4、void
// void代表无类型,通常使用在函数返回值上,代表函数无返回值(无return),但实际上js函数无return语句时会默认返回undefined,所以void被设计为兼容undefined类型,可以看作是无return和undefined的合体,所以void和其他类型交叉结果都是never,唯独undefined例外。
type t1 = void & number; // never
type t2 = void & undefined; // void,仍是合体
5、函数类型交叉计算:
函数比较特殊,因为它有参数和返回值两个地方涉及计算,两个地方因作用不同计算方式也不同。
5.1、让我们先看看参数的部分:
看这样两行代码,你猜编译时会抛出错误吗?
type Fn1 = (a: number) => void;
const fn: Fn1 = (a: number | string) => void;
答案是不会,你猜对了吗。那为什么不会呢?
主要是因为参数的目的是为了约束用户的输入,而函数实现只需兼容用户输入即可,不会造成安全问题,例如:
type Fn1 = (a: number) => void;
const fn: Fn1 = (a: number | string) => void;
fn(1); // 满足fn参数类型要求,且函数运行时可以处理number类型参数,运行时正常
fn(“a”); // 不满足fn参数类型要求,即便函数运行时可以处理string类型参数也会编译错误
所以,函数的参数在交叉计算时,新函数的参数类型只要兼容所有函数的参数类型即可。
接下来我们看一道关于函数参数交叉的题:
type Fn1 = (a: number) => void;
type Fn2 = (a: string) => void;
type Fn3 = Fn1 & Fn2; // 结果是怎样的呢?
答案是 (a: number | string) => void,其实ts内部并没有计算生成新的函数类型,但实际效果类似这样,我们验证一下:
const fn3: Fn3 = (a: number | string) => void; // 编译通过
fn3(1); // 编译通过
fn3("a"); // 编译通过
有没有觉得反直觉,参数交叉计算的结果居然是一个联合类型,背后的根本原因是函数参数类型约束的是用户输入,而不是函数实现,函数实现时参数部分可以定义为父类型兼容用户输入即可。
最后看下函数一个试验:
const fn3: Fn3 = (a: number | string | boolean): void => {}; // 编译通过(但调用时根据Fn3类型定义只能传number|string,否则编译错误)
const fn1: Fn1 = fn3; // 编译通过,fn3参数兼容fn1
const fn2: Fn2 = fn3; // 编译通过,fn3参数兼容fn2
const fn3: Fn3 = (a: number) => void; // 编译错误,不能反向赋值
总结:函数参数类型是为了约束用户输入的,故函数实现时参数可以为同类型或父类型。 (a: number | string) => void 可以看作是 (a: number) => void 的子类型,也就是A函数是B的子类型,那么A函数的参数是B函数参数的父类型,也就是参数类型关系与其函数类型关系相反。
5.2、接下来看看返回值的部分:
看这样两行代码,你猜编译时会抛出错误吗?
type Fn1 = () => number | string;
const fn: Fn1 = () => string
答案是不会,你猜对了吗。那为什么不会呢?
因为返回值的目的就是为了约束函数的返回,只要返回一个子类型即可满足类型约束即可,不会造成安全问题。例如:
type Fn1 = () => number | string;
const fn: Fn1 = () => “abc”;
const result = fn(); // number | string
result.substring(0, 1); // 编译错误:Property 'substring' does not exist on type 'number'。
接下来我们一道函数返回值交叉的例子:
type Fn1 = () => number;
type Fn2 = () => string;
type Fn3 = Fn1 & Fn2; // 结果是怎样的呢?
答案是:() => never,之前提到ts内部并没有计算生成新的函数类型,只是实际效果类似这样,我们验证下:
const fn32: Fn3 = (): number => 1; // 编译错误:Type 'number' is not assignable to type 'string'
const fn33: Fn3 = (): string => "a"; // 编译错误:Type 'string' is not assignable to type 'number'
const fn31: Fn3 = (): never => { throw "" }; // 编译通过
总结:函数返回值类型是为了约束函数自身返回的,故函数实现时可以返回同类型或子类型。 () => number 可以看作是 () => number | string 的子类型,也就是A函数是B的子类型,那么A函数的返回值也是B函数返回值的子类型,两者关系一致。
5.3、最后看个返回值的特例 void:
void作为函数返回值类型时代表无return,但是看这样两行代码,你猜编译时会抛出错误吗?
type Fn1 = () => void;
type fn1: Fn1 = () => "abc";
答案是:不会,那为什么呢?
从哲学上讲你不主动要东西而我白送你一个不算坏事,类比函数无返回值要求但我反你一个并不会造成安全影响,例:
const result = fn1();
result.substring(0,1); // 编译错误,Property 'substring' does not exist on type 'void'
那如果做交叉计算会怎样呢?
type Fn1 = () => void;
type Fn2 = () => string;
type Fn3 = Fn1 & Fn2; // 结果是怎样的呢?
答案是:() => string,之所以不是 never 的原因就是这不会造成安全影响,故函数返回值中的 void 交叉时会被收敛。
总结:函数返回值类型如果是void,实际实现返回了其他类型不会有安全影响,所以交叉计算时void会被收敛。
最终总结:
- unknown是所有类型的父类型,故:A & unknown 等价于 A
- never是所有类型的子类型,故:A & never 等价于 never
- any是为了逃逸类型检查,故:除never外任何类型 & any 等价于 any
- void为了兼容js函数特性所以兼容undefined类型,故:void & undefined 等价于 void
- 函数交叉时参数求的是父类型(约束用户输入),返回值求的是子类型(约束函数输出),如果返回值遇到void交叉计算时会被收敛为其它类型