Ts交叉类型真的懂了?试试函数交叉

525 阅读7分钟

注:本文需要你已经对Ts交叉类型有一定的了解。

先复习下类型可赋值关系(严格模式下):

anyunknownobjectvoidundefinednullnever
any →
unknown →
object →
void →
undefined →xx
null →xxx
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.1never
  
  // 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.2unknown
  
  // 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.3any
  
  // 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.4void
  
  // 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交叉计算时会被收敛为其它类型