TypeScript学习笔记-2-联合类型&交叉类型、泛型、类型守卫、类型推断

1,652 阅读10分钟

联合类型和交叉类型用于组合类型;泛型用于定义包含可复用类型的类型,就像函数一样可以传入类型变量;类型守卫是执行运行时类型检查来保证类型在一定范围内的表达式;类型推断指在一些没有明确的类型注释的地方使用推断出类型信息。

联合类型&交叉类型

刚开始学习联合类型(Union Types)和交叉类型(Intersection Types)的时候,因为有并集(Union)和交集(Intersection)的思想,所以总觉得哪里怪怪的。其实联合类型以及交叉类型和交集并集的概念不同,用联合类型以及交叉类型的时候不要和并集交集关联起来。

联合类型

1.联合类型表示一个值的类型可以是多个类型中的一种。使用|来分隔每一个类型。

let a: number | string;
a = 1;
a = '1';

2.一个联合类型只能访问所有类型都有的成员。

interface A1 {
  m1: number;
  m2: number;
}

interface A2 {
  m1: string;
  m3: boolean;
}

function a1 (x: A1 | A2): void {
  x.m1;
  x.m3; // 报错
}

上例中x.m1没问题,但是使用x.m3的时候,因为不能确保x一定包含m3属性,会报如下错误:

Property 'm3' does not exist on type 'A1 | A2'.
  Property 'm3' does not exist on type 'A1'.

A1 | A2类型的值是A1类型或A2类型,只能确保包含A1 | A2的共有成员。

3.当类型包含通用的字面量类型的字段时,可以通过通用成员的值来缩小类型的范围:

interface A3 {
  m1: 'One';
  m2: number;
}

interface A4 {
  m1: 'Two';
  m3: boolean;
}

function a2 (x: A3 | A4): void {
  const { m1 } = x;
  // 通过m1的值知道具体是哪个类型
  switch(m1) {
    case 'One':
      console.log((x as A3).m2);
      break;
    case 'Two':
      console.log((x as A4).m3);
      break;
    default:
  }
}

上面例子中A3类型和A4类型都包含字面量类型m1,通过switch语句来根据m1的值判断是什么类型。

4.当我们没有覆盖所有区别联合类型的变体的时候,编译器需要给出提示。

interface A3 {
  m1: 'One';
  m2: number;
}

interface A4 {
  m1: 'Two';
  m3: boolean;
}

function a2 (x: A3 | A4 ): string {
  const { m1 } = x;
  switch(m1) {
    case 'One':
      return String((x as A3).m2);
  }
}

a2函数的返回类型为string,当严格类型检查打开("strictNullChecks": true)的时候,这里会给出错误提示,因为没有覆盖m1Two的情况,所以这里返回类型可能为undefined

覆盖所有情况就不会有问题:

function a2 (x: A3 | A4 ): string {
  const { m1 } = x;
  switch(m1) {
    case 'One':
      return String((x as A3).m2);
    case 'Two':
      return String((x as A4).m3);
  }
}

交叉类型

交叉类型将多个类型合并为一个类型。交叉类型会包含所有类型的成员。

interface A5 {
  m1: number;
  m2: string;
}

interface A6 {
  m3: number;
}

interface A7 {
  m4: string;
}

type A8 = A5 & A6 & A7;

function a3 (x: A8): void {
  const { m1, m2, m3, m4 } = x;

  console.log(m1, m2, m3, m4);
}

type A8 = A5 & A6 & A7;A8类型包含A5A6A7类型的所有成员m1, m2, m3, m4

泛型

泛型用于定义包含可复用类型的类型。

function a4(x: string): string {
  return x;
}

这个函数的参数类型和返回值的类型是一样的,但是限制在只能是string。如果想要接收任意类型的参数,可以使用泛型:

function a5<T>(x: T): T {
  return x;
}

类型变量T会捕获用户提供的类型。类型变量T可以是类型的一部分,而不是整个类型:

function a6<T>(x: T[]): T[] {
  return x;
}

可以设置多个类型变量:

function a7<T1, T2>(x: T1 | T2): T1 | T2 {
  return x;
}

使用泛型函数之后,有两种方式调用它:

a5为例:

function a5<T>(x: T): T {
  return x;
}

1.明确设置T的类型。

const a8 = a5<string>('a');

这个表达式明确设置Tstring

2.使用类型参数推断。编译器会根据传入的参数自动设置T的值。

const a9 = a5('a');

泛型类型

1.泛型函数的类型和非泛型函数类似:

function b<T>(x: T): T {
  return x;
}

const b1: <T>(x: T) => T = b; 

泛型类型参数的名字不一定为T,等式两边的类型变量名也不一定要相同:

function b<T>(x: T): T {
  return x;
}

const b2: <U>(x: U) => U = b;

2.可以将一个泛型类型作为一个对象字面量的调用标志来写:

const b3: { <U>(x: U): U } = b;

3.泛型接口。

function b<T>(x: T): T {
  return x;
}

interface B<T> {
  (x: T): T;
}

const b4: B<string> = b;
b4(1); // 报错:Argument of type '1' is not assignable to parameter of type 'string'.

这里在使用的时候将T赋值为string类型const b4: B<string> = b;,所以b4(1)会报类型错误。

也可以这样写:

interface B1 {
  <T>(x: T): T;
}
const b5: B1 = b;
b5(1);

这里T并没有指定为特定类型,所以b5(1)不会报类型错误。

泛型类

泛型类和泛型接口形状类似:

class B2<T> {
  m1: T;
  m2: (x: T) => T;
}

const b6 = new B2<number>();
b6.m1 = 1;
b6.m2 = (x: number): number => x;

这里将T设置成了number,当然T还能是其他任意类型。

类有静态部分的类型和实例部分的类型,泛型类只覆盖类的实例部分,所以当使用类的时候,静态成员不能使用类的类型参数。

class B2<T> {
  static m3: T; // 报错:Static members cannot reference class type parameters.
  m1: T;
  m2: (x: T) => T;
}

泛型限制

可以创建一个接口,描述对泛型的限制。

interface B3 {
  m1: number;
}

function b7<T extends B3>(x: T): T {
  console.log(x.m1);
  return x;
}

上例中泛型被限制为必须包含m1属性,传入函数的参数必须包含m1属性。

可以定义一个被另一个类型参数限制的类型参数:

function b8<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

上例中类型参数K通过K extends keyof T被限制为T类型参数的关键字的联合类型。keyof关键字会拿到对象的类型并产生该对象的键的字符串/数字字面量并集

在泛型中使用类类型

function b9<T>(x: { new (): T }): T {
  return new x();
}

这个例子中的T表示类的实例类型,x是一个构造函数,通过new x()创建一个类实例并返回实例。

同样可以对泛型参数T进行限制:

class B4 {
  m1: number;
}

class B5 extends B4 {
  m2: string;
}

class B6 {
  m3: boolean;
}

function b10<T extends B4>(x: { new (): T }): T {
  return new x();
}
b10(B4);
b10(B5);
b10(B6); // 报错

上例z红,因为限制T是从B4扩展的,所以T必需包含B4的成员,否则就会报错。

b10(B6)报类型错误:

Argument of type 'typeof B6' is not assignable to parameter of type 'new () => B4'.
  Property 'm1' is missing in type 'B6' but required in type 'B4'.

B6改成下面这样再使用b10(B6);就没问题了,因为它包含了m1成员。

class B6 {
  m1: number;
  m3: boolean;
}

类型守卫

类型守卫是执行运行时类型检查来保证类型在一定范围内的表达式。

使用 in运算符

n in x表达式中,n是一个字符串字面量或字符串字面量类型,x是一个联合类型。当x中有一个可选或者必需的属性n的时候,n in xtrue,当x中没有属性n的时候n in xfalse

interface C {
  m1: number;
  m2?: string;
}

interface C1 {
  m3: number;
}

type C2 = C | C1;

function c(x: C2): void {
  if ('m1' in x) {
    console.log(x.m1);
  }
  if ('m4' in x) {
    console.log(x.m4); // 报错:Property 'm4' does not exist on type 'never'.
  }
}

类型谓词

使用类型谓词的形式是name is Type ,如果nameType类型的,name is Type 就为true

interface C3 {
  m1: number;
}

interface C4 {
  m2: number;
}

type C5 = C3 | C4;

function c1(x: C5): void {
  console.log(x.m1); // 报错
}

上例代码中直接使用x.m1会报错:

Property 'm1' does not exist on type 'C5'.
  Property 'm1' does not exist on type 'C4'.

因为m1只存在于C3中,当传入的xC4类型的时候,是没有m1的。所以需要先判断下传入的参数是类型C3还是类型C4

像下面这样写不会报错:

function isC3Type(x: C5): x is C3 {
  return (x as C3).m1 !== undefined;
}

function c1(x: C5): void {
  if (isC3Type(x)) {
    console.log(x.m1);
  }
}

typeof 类型守卫

如果联合类型中的类型都是原始类型的话,可以直接使用typeof来判断。typeof v === "typename"typename必需是numberstringboolean、或者symbol

function c2(x: string | number): void {
  if (typeof x === 'string') {
    console.log(x.split(','));
  } else {
    x.toFixed(2);
  }
}

instanceof 类型守卫

instanceof的右边需要是一个构造函数,TypeScript会按以下顺序缩小范围:

  1. 构造函数的prototype属性的类型(如果构造函数的prototype属性的类型不为any)。
  2. 类型的构造标志返回的类型的并集。
class C3 {
  m1: number;
}

function c3 (x: unknown): void {
  if (x instanceof C3) {
    console.log(x.m1);
  }
}

类型推断

TypeScript可以在一些没有明确的类型注释的地方使用类型推断来提供类型信息。

比如: let d = 5;,这里我们没有明确地使用类型注释let d: number = 5;,但是d仍然拥有类型number。这种直接的类型推断发生在下面场景:初始化变量、初始化成员、设置参数的默认值、确定函数的返回类型。

1.初始化变量:

let d = 1;
d = '1'; // 报错:Type '"1"' is not assignable to type 'number'.

d被推断为number类型,所以给它赋值字符串会报错。

2.初始化成员:

let d1 = {
  m1: 1,
};
d1.m1 = '1'; // 报错:Type '"1"' is not assignable to type 'number'.

m1被推断为number类型,所以给它赋值字符串会报错。

3.设置参数的默认值:

function d2(x = 1) {
  return x;
}

d2('1'); // 报错:Argument of type '"1"' is not assignable to parameter of type 'number | undefined'.

这里参数x的类型被推断为number | undefined

4.确定函数的返回类型:

function d3() {
  return 'd';
}

const d4 = d3();
d4.split(',');
d4.toFixed(2); // 报错:Property 'toFixed' does not exist on type 'string'.

这里d3()返回值的类型被推断为string

最佳通用类型推断

当从多个表达式中进行类型推断的时候,这些表达式的类型需要用来计算出一个“最佳通用类型”。

比如: const d5 = [1, '2'];,的最佳通用类型是(number | string)[]

上下文类型推断

当表达式的类型隐含在它所在的位置中时,上下文类型推断就会发生。

window.onload = (): void => {
  console.log('d');
};

在这个位置使用window.onload并不会报错window为定义,或者不能使用onload等,这是根据上下文推断出全局有window对象,并且window对象包含onload属性。

问题

学习了这一节之后,可以尝试为以下JavaScript声明类型:

// 假装x是用来放接口返回的数据的变量,它初始值为空对象{}
let x = {};

function requestData (response) {
  x = response;
  s(x);
}

function s({ m1, m2 }) {
  const arr = m1.split(', '); // 字符串对象有split方法
  const str = m2.toFixed(2); // 数字对象有toFixed方法
  console.log(arr, str);
}

我的解答:

// 假装x是用来放接口返回的数据的变量,它初始值为空对象{},我们不知道接口返回的数据会是什么,所以给它的类型是unknown
let x: unknown = {};

class E {
  m1: string;
  m2: number;
}

function requestData (response: unknown): void {
  x = response;
  if (x instanceof E) {
    s(x);
  }
}

function s({ m1, m2 }: E): void {
  const arr = m1.split(', '); // 字符串对象有split方法
  const str = m2.toFixed(2); // 数字对象有toFixed方法
  console.log(arr, str);
}
TypeScript学习笔记
《TypeScript学习笔记-1-基础类型、字面量类型、类型声明》
当前篇《TypeScript学习笔记-2-联合类型&交叉类型、泛型、类型守卫、类型推断》
下一篇《TypeScript学习笔记-3-枚举、函数、类、装饰器》
《TypeScript学习笔记-4-模块、命名空间》
《TypeScript学习笔记-5-tsc指令、TS配置、部分更新功能、通用类型》