TypeScript 入门指南

时间:2020-9-21 作者:admin

前言

其实快速入门 TypeScript 你只需要去阅读官方文档即可,之所以还需要总结本文的原因是,官方文档有些地方难以理解以及内容较多不易于快速上手。因此作者总结此文,旨在帮助同学快速上手 TypeScript 并且能轻易理解 TypeScript 晦涩难懂的部分。

TypeScript

  • 是 JavaScript 的超集;
  • 而且向 JavaScript 添加了可选的静态类型和基于类的面向对象编程;
  • 支持 ES6 到 ES10 甚至 ES-NEXT 的语法。

为什么使用 TS

优势:

  1. 程序更容易理解,例如函数的输入输出的参数类型,减少了手动对代码进行备注的工作量;
  2. 丰富的接口提示;
  3. 编译期间能够发现大部分错误;
  4. 完全兼容 JavaScript。

缺点:

  1. 增加了学习成本;
  2. 短期内增加了一些开发成本。

基础类型

TypeScript 基本类型,也就是可以被直接使用的单一类型。

数字

let binaryLiteral: number = 0b1010; // 二进制
let octalLiteral: number = 0o744; // 八进制
let decLiteral: number = 6; // 十进制
let hexLiteral: number = 0xf00d; // 十六进制

字符串

let name: string = "bob";

布尔值

let isDone: boolean = false;

any

字面意思,它可以表示任意类型。

let list: any[] = [1, true, "free"];

当你不确定将会收到一个什么类型的时候,通常会使用 any,但是切记滥用。在一些项目中很多程序员为了节约时间,多数类型都使用 any 代替,这样还不如不使用 TypeScript。

null 和 Undefined

TypeScript 里,undefined 和 null 两者各自有自己的类型分别叫做 undefined 和 null ,用处并不大。

let u: undefined = undefined;
let n: null = null;

void

void 类型像是与 any 类型相反,它表示没有任何类型。

主要使用场景是当一个函数没有返回值:

function warnUser(): void {
    console.log("This is my warning message");
}

上面代码表示 warnUser函数没有返回值。

我们应该都记得在 JavaScript 中,函数总是有返回值,要么返回具体值,要么返回 undefined。那么不禁有个疑问了,void 和 undefined 在 TypeScript 中的区别是什么,或者说我们该如何选择使用?

void vs undefined

void 应当仅仅用于函数声明,即没有明确返回值的函数应该被声明为 void 类型。如果用于变量声明,则只能为其赋值 null 或 undefined。

function foo (): void {
    console.log('foo');
}

const bar: undefined; 

unknown

就像所有类型都可以赋值给 any,所有类型也都可以赋值给 unknown。这使得 unknown 成为 TypeScript 类型系统的另一种顶级类型(另一种是 any)。

值赋给 unknown

let value: unknown;

value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK

对 value 变量的所有赋值都被认为是类型正确的。但是,当我们尝试将类型为 unknown 的值赋值给其他类型的变量时会发生什么?

unknown 类型赋给其它类型

unknown 类型只能被赋值给 any 类型和 unknown 类型本身。直观地说,这是有道理的,只有能够保存任意类型值的容器才能保存 unknown 类型的值。毕竟我们不知道变量 value 中存储了什么类型的值。

let value: unknown;

let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error

never

never 类型表示那些永不存在值的类型。

never 类型是 any 的子类型,也可以赋值给 any;然而没有类型是 never 的子类型除了 never 本身, 即使 any 也不可以赋值给 never。

# 返回never的函数必须存在无法达到的终点
function error(message: string): never {
    throw new Error(message);
}

# 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
    while (true) {
    }
}

object

object 表示非原始类型,也就是除 number,string,boolean,symbol,null 或 undefined 之外的类型。

declare function create(o: object | null): void;

create({ prop: 0 }); // OK
create(null); // OK

但其实我们通常不会这样去使用,通常会使用接口 interface 更加详细的表示一个对象。

枚举

使用枚举我们可以定义一些带名字的常量,TypeScript 支持数字枚举与字符串枚举。

数字枚举

enum Direction {
    Up,
    Down,
    Left,
    Right,
}

Up 的值为0,依次向后累加, Down = 1; Left = 2; Right = 3;

字符枚举

enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}

字符串枚举没有类似数字枚举自增长的行为。

复合类型

包含多个单一类型的类型。

数组类型

# []普通表示
let list: number[] = [1, 2, 3];

# 泛型表示
let list: Array<number> = [1, 2, 3];

元祖类型

元组类型表示一个已知元素数量和类型的数组,各元素的类型不必相同。

let x: [string, number];

x = ['hello', 10]; // 正确,类型都匹配
x = [10, 'hello']; // 错误,类型不匹配
x[3] = 'world'; // 正确, 字符串可以赋值给(string | number)类型
x[6] = true; // 错误, 布尔值不是(string | number)类型

接口类型

这里只讲解接口类型的基本用法,在下文中,会总结接口类型的详细用法。

interface LabelledValue {
  label: string;
}

function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label);
}

高级类型

如果一个类型不能满足要求这个时候,我们就要考虑使用联合类型,交叉类型来解决问题。

联合类型

我们先来看一段代码快速理解什么是联合类型:

const sayHello = (name: string | undefined) => { /* ... */ };

sayHello("semlinker");
sayHello(undefined);

上述代码中 name 的值要么是 string 要么是 undefined。

再来看一个更加复杂的例子:

interface Foo {
  foo: string;
  name: string;
}

interface Bar {
  bar: string;
  name: string;
}

const sayHello = (obj: Foo | Bar) => { /* ... */ };

sayHello({ foo: "foo", name: "lolo" });
sayHello({ bar: "bar", name: "growth" });

sayHello 函数的 obj 参数,要么满足 Foo 接口类型,要么满足 Bar 接口类型。

[注意] 在 sayHello 内部只能访问 obj.name,因为它是两种类型都包含的唯一属性。这个很好理解,因为函数并不知道你会传 Foo 类型还是 Bar 类型,因此函数中只能取它们的交集包含的类型

交叉类型

交叉类型是将多个类型合并为一个类型。

const sayHello = (obj: Foo & Bar) => { /* ... */ };

sayHello({ foo: "foo", bar: "bar", name: "kakuqo" });

现在 sayHello 要求 obj 参数同时包含 foo 和 bar 的属性。所以在 sayHello 内部,可以同时访问 obj.foo,obj.bar 和 obj.name。

关键字

typeof

在 TypeScript 中,typeof 操作符可以用来获取一个变量声明或对象的类型。

# 获取变量声明
interface Person {
  name: string;
  age: number;
}

const sem: Person = { name: 'semlinker', age: 30 };
type Sem = typeof sem; // -> Person

# 获取函数类型
function toArray(x: number): Array<number> {
  return [x];
}

type Func = typeof toArray; // -> (x: number) => number[]

keyof

keyof 操作符可以用来获取一个对象的所有 key 值:

interface Person {
  name: string;
  age: number;
}

type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join" 
type K3 = keyof { [x: string]: Person };  // string | number

in

in 用来遍历枚举类型:

type Keys = "a" | "b" | "c"

type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any, c: any }

infer

在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。

# 获取函数的返回值的实现

type ReturnType<T> = T extends ( ...args: any[]) => infer R ? R : any;

以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型,方便之后使用。

extends

有时候我们定义的泛型不想过于灵活或者想继承某些类,可以通过 extends 关键字添加泛型约束。

interface ILengthwise {
  length: number;
}

function loggingIdentity<T extends ILengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

loggingIdentity(3);  // 错误,数字类型不包含 length 属性

# 这时我们需要传入符合约束类型的值,必须包含必要的属性。
loggingIdentity({length: 10, value: 3});

类型别名

类型别名用来给一个类型起个新名字。

type Message = string | string[];

# 函数参数中使用 Message 类型别名,它是一个联合类型 string | string[]
let greet = (message: Message) => {
  // ...
};

类型使用

在 Typescript 中,类型通常在以下几种情况下使用。

变量中使用

let a: number;
let b: string;
let c: null;
let d: undefined;
let e: boolean;

类中使用

在类中使用方式和在变量中类似,只是提供了一些专门为类设计的静态属性、静态方法、成员属性、构造函数中的类型等。

class Greeter {
    static name:string = 'Greeter'
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

接口中使用

在接口中使用也比较简单,可以理解为组合多个单一类型。

interface IData {
  name: string;
  age: number;
  func: (s: string) => void;
}

函数中使用

在函数中使用类型时,主要用于处理函数参数、函数返回值。

# 函数参数
function a(all: string) {}

# 函数返回值
function a(a: string): string {}

# 可选参数
function a(a: number, b?: number) {}

类型断言

有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。

类型断言有两种语法形式:

“尖括号” 语法

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

as 语法

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

以上是 TypeScript 基础知识,基本上只要你熟悉 JavaScript 语法,那么你就可以快速上手,但是 TypeScript 远远不止这么简单。接下来我们来探索 TypeScript 的一些高级应用。

类型保护

类型保护允许你使用更小范围下的对象类型。TypeScript 熟知 JavaScript 中 instanceof 和 typeof 运算符的用法。如果你在一个条件块中使用这些,TypeScript 将会推导出在条件块中的的变量类型。

typeof

if (typeof x === 'string') {
    // 在这个块中,TypeScript 知道 `x` 的类型必须是 `string`
    console.log(x.subtr(1)); // Error: 'subtr' 方法并没有存在于 `string` 上
    console.log(x.substr(1)); // ok
  }

  x.substr(1); // Error: 无法保证 `x` 是 `string` 类型
}

TypeScript 将会辨别 string 上是否存在特定的函数,以及是否发生了拼写错误

instanceof

class Foo {
  foo = 123;
  common = '123';
}

class Bar {
  bar = 123;
  common = '123';
}

function doStuff(arg: Foo | Bar) {
  if (arg instanceof Foo) {
    console.log(arg.foo); // ok
    console.log(arg.bar); // Error
  }
  if (arg instanceof Bar) {
    console.log(arg.foo); // Error
    console.log(arg.bar); // ok
  }
}

doStuff(new Foo());
doStuff(new Bar());

TypeScript 甚至能够理解 else。当你使用 if 来缩小类型时,TypeScript 知道在其他块中的类型并不是 if 中的类型:

function doStuff(arg: Foo | Bar) {
  if (arg instanceof Foo) {
    console.log(arg.foo); // ok
    console.log(arg.bar); // Error
  } else {
    // 这个块中,一定是 'Bar'
    console.log(arg.foo); // Error
    console.log(arg.bar); // ok
  }
}

in

in 操作符可以安全的检查一个对象上是否存在一个属性,它通常也被作为类型保护使用:

interface A {
  x: number;
}

interface B {
  y: string;
}

function doStuff(q: A | B) {
  if ('x' in q) {
    // q: A
  } else {
    // q: B
  }
}

自定义类型保护的类型谓词

function isNumber(x: any): x is number {
  return typeof x === "number";
}

function isString(x: any): x is string {
  return typeof x === "string";
}

函数

TypeScript 函数与 JavaScript 函数的区别

TypeScript JavaScript
含有类型 无类型
箭头函数 箭头函数(ES2015)
函数类型 无函数类型
必填和可选参数 所有参数都是可选的
默认参数 默认参数
剩余参数 剩余参数
函数重载 无函数重载

参数类型和返回类型

function createUserId(name: string, id: number): string {
  return name + id;
}

函数类型

let idGenerator: (chars: string, nums: number) => string;

function createUserId(name: string, id: number): string {
  return name + id;
}

idGenerator = createUserId;

可选参数及默认参数

# 可选参数
function createUserId(name: string, id: number, age?: number): string {
  return name + id;
}

# 默认参数
function createUserId(
  name: string = "Semlinker",
  id: number,
  age?: number
): string {
  return name + id;
}

在声明函数时,可以通过 ? 号来定义可选参数,比如 age?: number这种形式。在实际使用时,需要注意的是可选参数要放在普通参数的后面,不然会导致编译错误。

剩余参数

function push(array, ...items) {
  items.forEach(function (item) {
    array.push(item);
  });
}

let a = [];
push(a, 1, 2, 3);

函数重载

函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。

就是为同一个函数提供多个函数类型定义进行函数重载,编译器会根据这个列表去处理函数的调用。

# 重载
function padding(all: number);
function padding(topAndBottom: number, leftAndRight: number);
function padding(top: number, right: number, bottom: number, left: number);

# 函数的实际实现
function padding(a: number, b?: number, c?: number, d?: number) {
  if (b === undefined && c === undefined && d === undefined) {
    b = c = d = a;
  } else if (c === undefined && d === undefined) {
    c = a;
    d = b;
  }
  return {
    top: a,
    right: b,
    bottom: c,
    left: d
  };
}

padding(1); // Okay: all
padding(1, 1); // Okay: topAndBottom, leftAndRight
padding(1, 1, 1, 1); // Okay: top, right, bottom, left

padding(1, 1, 1); // Error: Not a part of the available overloads

当 TypeScript 编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此在定义重载的时候,一定要把最精确的定义放在最前面。

接口

接口运行时的影响为 0。在 TypeScript 接口中有很多方式来声明变量的结构。

基础表示

interface Point {
  x: number;
  y: number;
}

可选属性

interface Point {
  x?: number;
  y: number;
}

只读属性

interface Point {
  readonly x: number;
  readonly y: number;
}

定义函数

interface SearchFunc {
  (source: string, subString: string): boolean;
}

索引类型

interface StringArray {
  [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

接口继承

interface Shape {
    color: string;
}

interface Square extends Shape {
    sideLength: number;
}

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;

定义类的类型

interface ClockInterface {
    currentTime: Date;
}

class Clock implements ClockInterface {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

构造函数

interface IClass {
  new (hour: number, minute: number);
}

let test2: IClass = class {
  constructor(x: number, y: number) {}
};

ES6 类的基本用法这里就不重复介绍了,重点介绍类中的高级用法:

  • 继承
  • 存储器 get set
  • readonly 修饰符
  • 公有,私有,受保护的修饰符
  • 抽象类 abstract

继承

继承 (Inheritance) 是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系。

class Animal {
  name: string;

  constructor(theName: string) {
    this.name = theName;
  }

  move(distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}

class Snake extends Animal {
  constructor(name: string) {
    super(name);
  }

  move(distanceInMeters = 5) {
    console.log("Slithering...");
    super.move(distanceInMeters);
  }
}

let sam = new Snake("Sammy the Python");
sam.move();

存储器 get set

对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

class Animal {
  get age(): number {
    return this._age;
  }
  set age(age: number) {
    this._age = age;
  }
}

修饰符(readonly、公有,私有,受保护)

class Animal {
  public name: string; // 公有属性,都可使用
  protected age: string; // 受保护的属性,本类和子类可以使用
  private address: string; // 私有属性,只有本类可以使用
  readonly height: number; // 只读修饰符
  static type: string; // 静态属性 Animal.type 使用
}

抽象类 abstract

在面向对象中,有一个比较重要的概念就是抽象类,抽象类用于类的抽象,可以定义一些类的公共属性、公共方法,让继承的子类去实现,也可以自己实现。

抽象类有以下两个特点:

  • 抽象类不能直接实例化;
  • 抽象类中的抽象属性和方法,必须被子类实现。

abstract class Animal {
  abstract makeSound(): void;

  # 定义方法实例
  move(): void {
    console.log("roaming the earch...");
  }
}

class Cat extends Animal {
  # 必须实现的抽象方法
  makeSound() {} 

    # 重写抽象类方法
  move() {
    console.log('move');
  }
}
new Cat3();

抽象类 vs 接口的区别

  • 抽象类要被子类继承,接口要被类实现;
  • 在 ts 中使用 extends 去继承一个抽象类,使用 implements 去实现一个接口;
  • 接口只能做方法声明,抽象类中可以作方法声明,也可以作方法实现;
  • 抽象类是有规律的,抽离一个类别的公共部分,而接口只是对相同属性和方法的抽象,属性和方法可以无任何关联。

泛型

在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据,这样用户就可以用自己的数据类型来使用组件。

设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是,类的实例成员、类的方法、函数参数和函数返回值。

泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型创建可复用的组件要更好,因为泛型会保留参数类型。

泛型函数

# 定义一个泛型变量
function identity<T>(arg: T): T {
    return arg;
}

# 定义多个泛型变量
function identity <T, U>(value: T, message: U) : T {
  console.log(message);
  return value;
}

其中 T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思:

  • K(Key):表示对象中的键类型;
  • V(Value):表示对象中的值类型;
  • E(Element):表示元素类型。

泛型接口

interface GenericIdentityFn<T> {
  (arg: T): T;
}

泛型类

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};

泛型约束

确保属性存在

function identity<T>(arg: T): T {
  console.log(arg.length); // Error
  return arg;
}

在这种情况下,编译器将不会知道 T 确实含有 length 属性,尤其是在可以将任何类型赋给类型变量 T 的情况下。我们需要做的就是让类型变量 extends 一个含有我们所需属性的接口,比如这样:

interface Length {
  length: number;
}

function identity<T extends Length>(arg: T): T {
  console.log(arg.length); // 可以获取length属性
  return arg;
}

检查对象上的键是否存在

泛型约束的另一个常见的使用场景就是检查对象上的键是否存在。

enum Difficulty {
  Easy,
  Intermediate,
  Hard
}

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

let tsInfo = {
   name: "Typescript",
   supersetOf: "Javascript",
   difficulty: Difficulty.Intermediate
}

let difficulty: Difficulty = 
  getProperty(tsInfo, 'difficulty'); // OK

let supersetOf: string = 
  getProperty(tsInfo, 'superset_of'); // Error

泛型工具类型

为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。出于篇幅考虑,这里我们只简单介绍其中几个常用的工具类型。

ReadOnly

将 T 中的类型都变为只读。

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};
  1. 通过 keyof T 拿到 T 的所有属性名;
  2. 使用 in 进行遍历,将值赋给 P;
  3. 通过 T[P] 取得相应的属性值;
  4. 对每个属性添加 readonly 修饰符,使其变为只读。

应用:

interface IPoint {
  x: number;
  y: number;
}

const start: Readonly<IPoint> = {
  x: 0,
  y: 0
}

Partial

将 T 中的类型都变为可选。

type Partial<T> = {
  [P in keyof T]?: T[P];
};
  • 通过 keyof T 拿到 T 的所有属性名;
  • 使用 in 进行遍历,将值赋给 P;
  • 通过 T[P] 取得相应的属性值;
  • 对每个属性添加 ?修饰符,使其变为可选。

应用:

interface Todo {
  title: string;
  description: string;
}

let newObj : Partial<Todo> = {
    title: "is title"
};

Record<K,T>

Record<K extends keyof any, T> 的作用是将 K 中所有的属性的值转化为 T 类型。

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

应用:

interface PageInfo {
  title: string;
}

type Page = "home" | "about" | "contact";

const x: Record<Page, PageInfo> = {
  about: { title: "about" },
  contact: { title: "contact" },
  home: { title: "home" }
};

Pick<T,K>

Pick<T, K extends keyof T> 的作用是将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型。

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

应用:

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false
};

Exclude<T, U>

Exclude<T, U> 的作用是将某个类型中属于另一个的类型移除掉。

type Exclude<T, U> = T extends U ? never : T;

如果 T 能赋值给 U 类型的话,那么就会返回 never 类型,否则返回 T 类型。最终实现的效果就是将 T 中某些属于 U 的类型移除掉。

type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number

执行原理解析:

type T0 = Exclude<"a" | "b" | "c", "a">;

# 解析过程
type T0 = ('a' extends 'a' ? never : 'a') |
          ('b' extends 'a' ? never : 'b') |
          ('c' extends 'a' ? never : 'c') |

# 结果
type T0 =  'b' | 'c' ;

ReturnType

ReturnType 的作用是用于获取函数 T 的返回类型。

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

应用:

type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<(s: string) => void>; // void
type T2 = ReturnType<<T>() => T>; // {}
type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // number[]
type T4 = ReturnType<any>; // any
type T5 = ReturnType<never>; // any
type T6 = ReturnType<string>; // Error
type T7 = ReturnType<Function>; // Error

类型兼容性

在 TypeScript 中是通过结构体来判断兼容性的,如果两个的结构体一致,就直接兼容了,但如果不一致,TypeScript 给我们提供了以下两种兼容方式:

A = B这个表达式为例:

  • 协变,表示 B 的结构体必须包含 A 中的所有结构,即 B 中的属性可以比 A 多,但不能少;
  • 逆变,和协变相反,即 B 中的所有属性都在 A 中能找到,可以比 A 的少;
  • 双向协变,即没有规则,B 中的属性可以比 A 多,也可以比 A 少。

对象中的兼容

对象中的兼容,采用的是协变。

let obj1 = {
  a: 1,
  b: "b",
  c: true,
};

let obj2 = {
  a: 1,
};

obj2 = obj1;
obj1 = obj2; // 报错,因为 obj2 属性不够

函数返回值兼容

函数返回值中的兼容,采用的是协变。

let fun1 = function (): { a: number; b: string } {
  return { a: 1, b: "" };
};
let fun2 = function (): { a: number } {
  return { a: 1 };
};

fun1 = fun2; // 报错,fun2 中没有 b 参数
fun2 = fun1;

函数参数个数兼容

函数参数个数的兼容,采用的是逆变。

// 如果函数中的所有参数,都可以在赋值目标中找到,就能赋值
let fun1 = function (a: number, b: string) {};
let fun2 = function (a: number) {};

fun1 = fun2;
fun2 = fun1; // 报错,fun1 中的 b 参数不能在 fun2 中找到

函数参数兼容

函数参数兼容,采用的是双向协变。

let fn1 = (a: { name: string; age: number }) => {
  console.log("使用 name 和 age");
};
let fn2 = (a: { name: string }) => {
  console.log("使用 name");
};

fn2 = fn1; // 正常
fn1 = fn2; // 正常

类中的兼容

类中的兼容,是在比较两个实例中的结构体,是一种协变。

class Student1 {
  name: string;
  // private weight:number
}

class Student2 {
  // extends Student1
  name: string;
  age: number;
}

let student1 = new Student1();
let student2 = new Student2();

student1 = student2;
student2 = student1; // 报错,student1 没有 age 参数

需要注意的是,实例中的属性和方法会受到类中修饰符的影响,如果是 private 修饰符,那么必须保证两者之间的 private 修饰的属性来自同一对象。如上文中如果把 private 注释放开的话,只能通过继承去实现兼容。

泛型中的兼容

泛型中的兼容,如果没有用到 T,则两个泛型也是兼容的。

interface Empty<T> {}
let x1: Empty<number>;
let y1: Empty<string>;

x1 = y1;
y1 = x1;

小结

作者尽量保证文章结构清晰,阅读通顺,没有明显错别字,以及文章内容可靠性高。如有任何问题欢迎留言指正,共同学习。

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。