之前 TS 笔记由于放在长文章后面阅读体验不太好,故单独抽离出来一篇文章。

本文主要记录了 TS 的学习知识与笔记。

基本类型

TS 有以下几种数据类型:

  • 基础类型:包括boolean(布尔值)、number(数字)、string(字符串)、null(空值)、undefined(未定义值)、bigint(大整型)和symbol(符号)等。这些类型和 JavaScript 的基本类型基本一致,只是 TS 在编译时会检查变量的类型是否匹配。例如:

    1
    2
    3
    4
    5
    6
    7
    let isDone: boolean = false; // 声明一个布尔类型的变量
    let age: number = 18; // 声明一个数字类型的变量
    let name: string = "Alice"; // 声明一个字符串类型的变量
    let x: null = null; // 声明一个空值类型的变量
    let y: undefined = undefined; // 声明一个未定义值类型的变量
    let a: bigint = 2172141653n; // 定义一个大整型变量
    let z: symbol = Symbol("key"); // 声明一个符号类型的变量
  • 数组类型:用来表示一组相同类型的数据。TS 有两种方式可以定义数组类型,一种是在元素类型后面加上[],另一种是使用泛型Array<元素类型>。例如:

    1
    2
    let arr1: number[] = [1, 2, 3]; // 声明一个数字类型的数组
    let arr2: Array<number> = [4, 5, 6]; // 声明一个数字类型的数组,使用泛型
  • 元组类型:用来表示一个已知元素数量和类型的数组,各元素的类型不必相同,但是对应位置的类型需要相同。例如:

    1
    2
    3
    let tuple: [string, number]; // 声明一个元组类型的变量
    tuple = ["Bob", 20]; // 赋值正确,字符串和数字类型分别对应
    tuple = [20, "Bob"]; // 赋值错误,类型不匹配
  • 枚举类型:用来定义一组有名字的常数,可以方便地访问和使用。TS 支持数字枚举和字符串枚举,还可以使用const关键字定义常量枚举,以提高性能。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    enum Color {
      Red,
      Green,
      Blue,
    } // 声明一个数字枚举类型
    let c: Color = Color.Blue; // 赋值正确,c的值为2
    console.log(c); // 输出2
    
    enum Direction {
      Up = "UP",
      Down = "DOWN",
      Left = "LEFT",
      Right = "RIGHT",
    } // 声明一个字符串枚举类型
    let d: Direction = Direction.Left; // 赋值正确,d的值为'LEFT'
    console.log(d); // 输出'LEFT'
    
    const enum Month {
      Jan,
      Feb,
      Mar,
    } // 声明一个常量枚举类型
    let m: Month = Month.Feb; // 赋值正确,m的值为1
    console.log(m); // 输出1
  • any 类型:用来表示任意类型的数据,可以赋值给任何类型的变量,也可以接受任何类型的赋值。这样可以避免类型检查的错误,但是也会失去类型检查的好处。例如:

    1
    2
    3
    let a: any = 1; // 声明一个任意类型的变量
    a = "hello"; // 赋值正确,可以赋值为字符串类型
    a = true; // 赋值正确,可以赋值为布尔类型
  • void 类型:用来表示没有任何类型,一般用于标识函数的返回值类型,表示该函数没有返回值。例如:

    1
    2
    3
    4
    function sayHello(): void {
      // 声明一个返回值为void类型的函数
      console.log("Hello");
    }
  • never 类型:用来表示永远不会出现的值的类型,例如抛出异常或无限循环的函数的返回值类型。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function error(message: string): never {
      // 声明一个返回值为never类型的函数
      throw new Error(message); // 抛出异常
    }
    
    function loop(): never {
      // 声明一个返回值为never类型的函数
      while (true) {} // 无限循环
    }
  • unknown 类型unknown类型和any类型有些相似,但是更加安全,因为它不允许对未经类型检查的值进行任何操作,除非使用类型断言或类型收缩来缩小范围。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    let u: unknown = "Hello"; // 声明一个unknown类型的变量
    u = 10; // 赋值正确,可以赋值为任何类型
    console.log(u + 1); // 错误,不能对unknown类型进行运算
    console.log((u as number) + 1); // 正确,使用类型断言缩小范围
    if (typeof u === "number") {
      // 正确,使用类型收缩缩小范围
      console.log(u + 1);
    }
查看 never 类型与 void 类型的区别

TS 中的 never 类型和 void 类型是两种特殊的类型,它们的用法和含义有一些区别:

  • never 类型:用来表示永远不会出现的值的类型,例如抛出异常或无限循环的函数的返回值类型。never类型是任何类型的子类型,也就是说never类型的值可以赋值给任何类型的变量,但是没有类型的值可以赋值给never类型的变量(除了never本身)。这意味着never类型可以用来进行详尽的类型检查,避免出现不可能的情况。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    function error(message: string): never {
      // 声明一个返回值为never类型的函数
      throw new Error(message); // 抛出异常
    }
    
    function loop(): never {
      // 声明一个返回值为never类型的函数
      while (true) {} // 无限循环
    }
    
    function check(x: string | number) {
      switch (typeof x) {
        case "string":
          // do something
          break;
        case "number":
          // do something
          break;
        default:
          const never: never = x; // 错误,x的类型不可能是never
        // do something
      }
    }
  • void 类型:用来表示没有任何类型,一般用于标识函数的返回值类型,表示该函数没有返回值。void 类型的变量只能赋值为undefinednull(在严格模式下,只能赋值为undefined)。void类型的作用是避免不小心使用了空指针导致的错误,和 C 语言中的void是类似的。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function sayHello(): void {
      // 声明一个返回值为void类型的函数
      console.log("Hello");
    }
    
    let x: void = undefined; // 声明一个void类型的变量
    x = null; // 赋值正确,如果不是严格模式
    x = 1; // 错误,不能赋值为其他类型
    console.log(x); // 输出undefined或null
查看 any 类型与 unknown 类型的区别

TSany类型与unknown类型是两种特殊的类型,它们都可以接受任何类型的值,但它们之间有一些重要的区别。下面我将从以下几个方面来讲解这两种类型的特点和用法,以及它们的异同:

  • 定义和赋值any类型是TS中最宽泛的类型,它表示任意类型的值,可以赋值给任何类型的变量,也可以接受任何类型的值。unknown类型是 TS 3.0 中引入的一种新的类型,它表示未知类型的值,也可以赋值给任何类型的变量,也可以接受任何类型的值。例如:

    1
    2
    3
    4
    5
    6
    let a: any; // 定义一个any类型的变量a
    let b: unknown; // 定义一个unknown类型的变量b
    a = 1; // 可以给a赋值为数字
    a = "hello"; // 可以给a赋值为字符串
    b = 2; // 可以给b赋值为数字
    b = "world"; // 可以给b赋值为字符串
  • 操作和访问any类型的变量可以进行任何操作和访问,不会有类型检查的错误,但这也会导致一些潜在的问题,比如访问不存在的属性或方法,或者调用不合法的参数。unknown 类型的变量则不能进行任何操作和访问,除非进行类型断言或类型收缩,否则会有类型检查的错误,这样可以保证类型的安全性。例如:

    1
    2
    3
    4
    5
    6
    let a: any;
    let b: unknown;
    a.foo(); // 可以调用任意的方法,不会报错,但可能运行时出错
    a + 1; // 可以进行任意的运算,不会报错,但可能得到意外的结果
    b.foo(); // 不能调用任意的方法,会报错:Object is of type 'unknown'
    b + 1; // 不能进行任意的运算,会报错:Object is of type 'unknown'
  • 赋值给其他类型any类型的变量可以赋值给任何类型的变量,不会有类型检查的错误,但这也会导致一些潜在的问题,比如赋值给不兼容的类型,或者覆盖了原有的类型信息。unknown类型的变量则只能赋值给any类型或unknown类型的变量,否则会有类型检查的错误,这样可以保证类型的一致性。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    let a: any;
    let b: unknown;
    let c: number;
    let d: string;
    c = a; // 可以把any类型赋值给number类型,不会报错,但可能赋值不合法的值
    d = a; // 可以把any类型赋值给string类型,不会报错,但可能赋值不合法的值
    c = b; // 不能把unknown类型赋值给number类型,会报错:Type 'unknown' is not assignable to type 'number'
    d = b; // 不能把unknown类型赋值给string类型,会报错:Type 'unknown' is not assignable to type 'string'
  • 类型断言:类型断言是一种告诉编译器我们比它更了解类型的方式,它可以让我们强制把一个类型转换为另一个类型,但这也有一定的风险,比如断言不合法的类型,或者忽略了一些类型检查。any类型的变量可以使用类型断言转换为任何类型,不会有类型检查的错误,但这也会导致一些潜在的问题,比如断言错误的类型,或者丢失了类型信息。unknown类型的变量则可以使用类型断言转换为任何类型,但这需要我们明确地指定要转换的类型,这样可以保证类型的正确性。例如:

    1
    2
    3
    4
    5
    6
    let a: any;
    let b: unknown;
    let c = a as number; // 可以把any类型断言为number类型,不会报错,但可能断言错误的类型
    let d = a as string; // 可以把any类型断言为string类型,不会报错,但可能断言错误的类型
    let e = b as number; // 可以把unknown类型断言为number类型,不会报错,但需要明确指定类型
    let f = b as string; // 可以把unknown类型断言为string类型,不会报错,但需要明确指定类型
  • 类型收缩:类型收缩是一种让编译器自动推断出更具体的类型的方式,它可以让我们根据一些条件判断来缩小类型的范围,从而进行一些操作和访问。any类型的变量不能使用类型收缩,因为它已经是最宽泛的类型,没有更具体的类型可以推断出来。unknown类型的变量则可以使用类型收缩,通过一些类型保护的方法,比如typeofinstanceofin等,来推断出更具体的类型,从而进行一些操作和访问。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    let a: any;
    let b: unknown;
    if (typeof a === "number") {
      a.toFixed(2); // 不能使用类型收缩,会报错:Object is of type 'any'
    }
    if (typeof b === "number") {
      b.toFixed(2); // 可以使用类型收缩,不会报错,推断出b是number类型
    }
查看 ts 中的元组 tuple

TS 中的元组是一种特殊的数组,它可以存储不同类型的元素,并且元素的个数和类型在定义时就已经确定了。元组的语法格式如下:

1
let tuple_name: [type1, type2, ..., typeN] = [value1, value2, ..., valueN];

例如,我们可以定义一个元组,包含一个字符串和一个数字:

1
let mytuple: [string, number] = ["Hello", 42];

元组的元素可以通过索引来访问,索引从 0 开始,例如:

1
2
console.log(mytuple[0]); // 输出 "Hello"
console.log(mytuple[1]); // 输出 42

元组的长度和类型都是固定的,所以不能越界访问或修改元素,也不能添加或删除超出范围的元素。否则,TS 编译器会报错。但是,我们可以对元组的元素进行更新操作,例如:

1
2
mytuple[0] = "World"; // 更新第一个元素
console.log(mytuple[0]); // 输出 "World"

元组还有一些与其相关的方法,主要有以下几种:

  • push():向元组的末尾添加一个新元素,返回新的长度。注意,这个方法会改变原来的元组,而且添加的元素必须是元组中已有类型的联合类型。
  • pop():从元组的末尾移除一个元素,返回被移除的元素。注意,这个方法会改变原来的元组。
  • concat():连接两个元组,返回一个新的元组。注意,这个方法不会改变原来的元组,而且连接后的元组的类型必须是两个元组的类型的联合类型。
  • slice():从元组中截取一部分元素,返回一个新的元组。注意,这个方法不会改变原来的元组,而且截取后的元组的类型必须是原来元组的类型的联合类型。

下面是一些使用这些方法的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let mytuple: [string, number] = ["Hello", 42];
let yourtuple: [string, number] = ["true", 100];

mytuple.push("World"); // 添加一个字符串元素
console.log(mytuple); // 输出 ["Hello", 42, "World"]

let last = mytuple.pop(); // 移除最后一个元素
console.log(last); // 输出 "World"
console.log(mytuple); // 输出 ["Hello", 42]

let newtuple = mytuple.concat(yourtuple); // 连接两个元组
console.log(newtuple); // 输出 ["Hello", 42, 'true', 100]

let subtuple = newtuple.slice(1, 3); // 截取一部分元组
console.log(subtuple); // 输出 [42, 'true']

枚举类型

枚举类型是一种在 TypeScript 中定义一组带名字的常量的方式。枚举可以清晰地表达意图或创建一组有区别的用例。TypeScript 支持基于数字和基于字符串的枚举。

1
2
3
4
5
6
7
enum Direction {
  Up,
  Down = 10,
  Left,
  Right,
}
console.log(Direction.Up, Direction.Down, Direction.Left, Direction.Right); // 0 10 11 12
  • 数字枚举:每个成员都有一个数字值,可以是常量或计算出来的。如果没有初始化器,第一个成员的值为 0,后面的成员依次递增。例如:

    1
    2
    3
    4
    5
    6
    enum Direction {
      Up, // 0
      Down, // 1
      Left, // 2
      Right, // 3
    }
  • 字符串枚举:每个成员都必须用字符串字面量或另一个字符串枚举成员初始化。字符串枚举没有自增长的行为,但可以提供一个运行时有意义的并且可读的值。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    enum Direction {
      Up = "UP",
      Down = "DOWN",
      Left = "LEFT",
      Right = "RIGHT",
    }
    console.log(Direction["Right"], Direction.Up); // RIGHT UP
    // 后续也需要设置字符串
    enum Direction {
      Up = "UP",
      Down, // error TS1061: Enum member must have initializer
      Left, // error TS1061: Enum member must have initializer
      Right, // error TS1061: Enum member must have initializer
    }
  • 异构枚举:可以混合字符串和数字成员,但不建议这么做。例如:

    1
    2
    3
    4
    enum BooleanLikeHeterogeneousEnum {
      No = 0,
      Yes = "YES",
    }
  • 常量枚举:使用const enum关键字定义,只能使用常量枚举表达式初始化成员,不能包含计算或动态的值。常量枚举在编译阶段会被删除,不会生成任何代码。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const enum Direction {
      Up,
      Down,
      Left,
      Right,
    }
    
    let directions = [
      Direction.Up,
      Direction.Down,
      Direction.Left,
      Direction.Right,
    ]; // 编译后变为 [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */]
  • 联合枚举和枚举成员类型:当所有枚举成员都是字面量类型时(不带有初始值或者初始化为字符串或数字字面量),枚举成员本身就是类型,而枚举类型本身就是每个成员的联合类型。这样可以实现更精确的类型检查和约束。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    enum ShapeKind {
      Circle,
      Square,
    }
    
    interface Circle {
      kind: ShapeKind.Circle; // 只能是 ShapeKind.Circle 类型
      radius: number;
    }
    
    interface Square {
      kind: ShapeKind.Square; // 只能是 ShapeKind.Square 类型
      sideLength: number;
    }
    
    let c: Circle = {
      kind: ShapeKind.Square, // Error! 类型不匹配
      radius: 100,
    };
  • 运行时的枚举:枚举是在运行时真正存在的对象,可以作为参数传递给函数或从函数返回。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    enum E {
      X,
      Y,
      Z,
    }
    
    function f(obj: { X: number }) {
      return obj.X;
    }
    
    // Works, since 'E' has a property named 'X' which is a number.
    f(E);
  • 反向映射:数字枚举成员具有反向映射,可以根据枚举值得到对应的名字。例如:

    1
    2
    3
    4
    5
    6
    enum Enum {
      A,
    }
    
    let a = Enum.A;
    let nameOfA = Enum[a]; // "A"
查看枚举的本质

一个枚举的案例如下

1
2
3
4
5
6
7
8
enum Direction {
  Up,
  Down,
  Left,
  Right,
}
console.log(Direction.Up === 0); // true
console.log(Direction[0], typeof Direction[0]); // Up string

编译后的js代码

1
2
3
4
5
6
7
8
9
10
11
12
var Direction;
(function (Direction) {
  Direction[(Direction["Up"] = 0)] = "Up";
  Direction[(Direction["Down"] = 1)] = "Down";
  Direction[(Direction["Left"] = 2)] = "Left";
  Direction[(Direction["Right"] = 3)] = "Right";
})(Direction || (Direction = {}));
(function (Direction) {
  Direction[(Direction["Center"] = 1)] = "Center";
})(Direction || (Direction = {}));
console.log(Direction.Up === 0); // true
console.log(Direction[0], typeof Direction[0]); // Up string

接口

ts 中的接口是一种用来描述对象的形状(shape)的语法,它可以规定对象的属性和方法,以及它们的类型。接口可以让我们在编写代码时进行类型检查,避免出现类型错误。接口也可以提高代码的可读性和可维护性,让我们更清楚地知道对象的结构和功能。

ts 中的接口有以下几个特点:

  • 可选属性:接口中的属性可以用?标记为可选,表示这个属性可以存在也可以不存在。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    interface Person {
      name: string; // 必须属性
      age?: number; // 可选属性
    }
    
    let p1: Person = { name: "Alice" }; // 合法,age可以省略
    let p2: Person = { name: "Bob", age: 18 }; // 合法,age可以存在
    let p3: Person = { name: "Charlie", gender: "male" }; // 非法,gender不是接口定义的属性
  • 只读属性:接口中的属性可以用readonly标记为只读,表示这个属性只能在对象创建时赋值,不能再修改。例如:

    1
    2
    3
    4
    5
    6
    7
    interface Point {
      readonly x: number; // 只读属性
      readonly y: number; // 只读属性
    }
    
    let p1: Point = { x: 10, y: 20 }; // 合法,创建时赋值
    p1.x = 30; // 非法,不能修改只读属性
  • 函数类型:接口中可以定义函数的类型,即参数列表和返回值类型。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    interface SearchFunc {
      (source: string, subString: string): boolean; // 函数类型
    }
    
    let mySearch: SearchFunc; // 定义一个变量符合函数类型
    mySearch = function (src, sub) {
      // 实现一个函数符合函数类型
      let result = src.search(sub);
      return result > -1;
    };
  • 索引类型:接口中可以定义索引的类型,即通过[]访问对象的类型。索引可以是数字或字符串。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    interface StringArray {
      [index: number]: string; // 索引类型
    }
    
    let myArray: StringArray; // 定义一个变量符合索引类型
    myArray = ["Bob", "Fred"]; // 赋值一个数组符合索引类型
    
    let myStr: string = myArray[0]; // 访问数组元素符合索引类型
  • 类类型:接口中可以定义类的类型,即类的构造函数和实例方法。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    interface ClockInterface {
      currentTime: Date; // 实例属性
      setTime(d: Date): void; // 实例方法
    }
    
    class Clock implements ClockInterface {
      // 类实现接口
      currentTime: Date;
      constructor(h: number, m: number) {
        this.currentTime = new Date();
        this.currentTime.setHours(h);
        this.currentTime.setMinutes(m);
      }
      setTime(d: Date) {
        this.currentTime = d;
      }
    }
  • 继承接口:接口之间可以相互继承,从而拥有父接口的属性和方法。一个接口也可以继承多个接口,实现多重继承。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    interface Shape {
      color: string; // 父接口属性
    }
    
    interface PenStroke {
      penWidth: number; // 父接口属性
    }
    
    interface Square extends Shape, PenStroke {
      // 子接口继承两个父接口
      sideLength: number; // 子接口属性
    }
    
    let square = {} as Square; // 定义一个变量符合子接口类型
    square.color = "blue"; // 赋值父接口Shape的属性
    square.sideLength = 10; // 赋值子接口Square的属性
    square.penWidth = 5.0; // 赋值父接口PenStroke的属性

类(class)

TS中的类的基本使用与TS相同,只不过引入了类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Car {
  // 字段
  engine: string;

  // 构造函数
  constructor(engine: string) {
    this.engine = engine;
  }

  // 方法
  disp(): void {
    console.log("发动机为 :   " + this.engine);
  }
}

TS相对于JS中的class也添加了一些更高阶的类的特性。

访问修饰符

TS引入访问修饰符,类似JAVA。值得注意的是,访问修饰符的特性是TS本身的规范,JS并不能实现类似访问修饰符的特性。

  • public:公开,可以自由的访问类程序里定义的成员。
  • private:私有,只能在类的内部进行访问。
  • protected:受保护,除了在该类的内部可以访问,还可以在子类中仍然可以访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class Animal {
  public name: string; // 公开的,可以在任何地方访问
  private age: number; // 私有的,只能在类的内部访问
  protected color: string; // 受保护的,可以在类的内部和子类中访问
  readonly species: string; // 只读的,只能在声明时或构造函数中赋值

  constructor(name: string, age: number, color: string, species: string) {
    this.name = name;
    this.age = age;
    this.color = color;
    this.species = species;
  }

  // 访问器,用来获取或设置私有或受保护的成员
  get Age() {
    return this.age;
  }

  set Age(value: number) {
    if (value > 0) {
      this.age = value;
    }
  }

  get Color() {
    return this.color;
  }

  set Color(value: string) {
    this.color = value;
  }
}

class Cat extends Animal {
  constructor(name: string, age: number, color: string) {
    super(name, age, color, "cat"); // 调用父类的构造函数
  }

  // 重写父类的方法
  get Color() {
    return "The color of this cat is " + super.Color; // 访问父类的受保护成员
  }
}

let animal = new Animal("Tom", 3, "black", "dog");
console.log(animal.name); // 可以访问公开成员
// console.log(animal.age); // 错误,不能访问私有成员
console.log(animal.Age); // 可以通过访问器访问私有成员
// console.log(animal.color); // 错误,不能访问受保护成员
console.log(animal.Color); // 可以通过访问器访问受保护成员
console.log(animal.species); // 可以访问只读成员
// animal.species = "bird"; // 错误,不能修改只读成员

let cat = new Cat("Jerry", 2, "white");
console.log(cat.name); // 可以访问公开成员
// console.log(cat.age); // 错误,不能访问私有成员
console.log(cat.Age); // 可以通过访问器访问私有成员
// console.log(cat.color); // 错误,不能访问受保护成员
console.log(cat.Color); // 可以通过访问器访问受保护成员,注意这里调用的是子类重写的方法
console.log(cat.species); // 可以访问只读成员
// cat.species = "mouse"; // 错误,不能修改只读成员

TS还引入了属性访问修饰符readonly。注意:readonly只能用于修饰属性,可以配合classclassfield写法例如:

1
2
3
4
5
6
7
8
9
10
11
class Person {
  public readonly name: string; // 公有的只读属性
  private readonly age: number; // 私有的只读属性
  protected readonly gender: string; // 受保护的只读属性

  constructor(name: string, age: number, gender: string) {
    this.name = name;
    this.age = age;
    this.gender = gender;
  }
}

抽象类

TS 中的抽象类是一种特殊的类,它不能被直接实例化,只能作为其他类的基类来提供通用的属性和方法的定义。抽象类主要用于定义一组相关的类的共同结构和行为,以及强制子类实现特定的方法。抽象类用 abstract 关键字修饰,抽象类中的抽象方法也用 abstract 关键字修饰,抽象方法没有具体的实现,只有声明。抽象类可以有构造器,也可以有非抽象的属性和方法。抽象类的子类必须实现抽象类中的所有抽象方法,否则也会成为抽象类。
以下是一个 TS 中抽象类的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 定义一个抽象类Animal,它有一个name属性,一个构造器,一个sayHello方法和一个抽象的eat方法
abstract class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  sayHello(): void {
    console.log(`Hello, I am ${this.name}`);
  }
  abstract eat(): void; // 抽象方法,没有实现
}

// 定义一个子类Dog,它继承了Animal类,它必须实现Animal类中的抽象方法eat
class Dog extends Animal {
  constructor(name: string) {
    super(name); // 调用父类的构造器
  }
  eat(): void {
    // 实现抽象方法
    console.log(`${this.name} is eating bones`);
  }
}

// 定义一个子类Cat,它继承了Animal类,它必须实现Animal类中的抽象方法eat
class Cat extends Animal {
  constructor(name: string) {
    super(name); // 调用父类的构造器
  }
  eat(): void {
    // 实现抽象方法
    console.log(`${this.name} is eating fish`);
  }
}

// 创建一个Dog对象和一个Cat对象
let dog = new Dog("Tommy");
let cat = new Cat("Kitty");

// 调用它们的方法
dog.sayHello(); // Hello, I am Tommy
dog.eat(); // Tommy is eating bones
cat.sayHello(); // Hello, I am Kitty
cat.eat(); // Kitty is eating fish

// 不能创建一个Animal对象,因为Animal是抽象类
let animal = new Animal("Jack"); // 编译错误,不能实例化抽象类

函数

  1. ts类型定义

    1
    2
    3
    4
    5
    6
    7
    // 方式一,函数类型的对象字面量,可用于函数重载
    type LongHand = {
      (a: number): number;
    };
    
    // 方式二,函数类型的别名,只能定义一个函数的类型,而不能定义多个函数的类型,不可用于函数重载
    type ShortHand = (a: number) => number;

    重载签名的作用是为了让 TS 编译器能够正确地推断函数的参数类型和返回类型,从而提供更好的类型检查和代码提示。如果没有重载签名,函数依旧能够发挥作用,但是 TS 编译器可能无法识别函数的参数类型和返回类型,导致类型错误或警告。例如,如果你在 TS 中使用以下的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 没有重载签名的函数
    function add(x: any, y: any): any {
      // 函数体
      if (typeof x === "string" && typeof y === "string") {
        // 字符串相拼接
        return x + y;
      } else if (typeof x === "number" && typeof y === "number") {
        // 数字相加
        return x + y;
      } else {
        // 抛出错误
        throw new Error("Invalid arguments");
      }
    }
    
    // 调用函数
    let a = add("Hello", "World"); // a的类型是any
    let b = add(1, 2); // b的类型是any
    let c = add("1", 2); // c的类型是any,但是会抛出错误

    你会发现,变量 a、b 和 c 的类型都是 any,这意味着 TS 编译器无法确定它们的具体类型,也就无法提供类型检查和代码提示。例如,如果你想对 a 进行字符串操作,或者对 b 进行数学运算,TS 编译器可能会提示你这样做是不安全的,因为它们可能不是你期望的类型。而如果你使用重载签名,TS 编译器就能够根据你传入的参数类型,推断出函数的返回类型,从而提供更好的类型检查和代码提示。例如,如果你在 TS 中使用以下的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 有重载签名的函数
    function add(x: string, y: string): string;
    function add(x: number, y: number): number;
    
    function add(x: any, y: any): any {
      // 函数体
      if (typeof x === "string" && typeof y === "string") {
        // 字符串相拼接
        return x + y;
      } else if (typeof x === "number" && typeof y === "number") {
        // 数字相加
        return x + y;
      } else {
        // 抛出错误
        throw new Error("Invalid arguments");
      }
    }
    
    // 调用函数
    let a = add("Hello", "World"); // a的类型是string
    let b = add(1, 2); // b的类型是number
    let c = add("1", 2); // c的类型是any,但是会抛出错误

    你会发现,变量 a 的类型是 string,变量 b 的类型是 number,这意味着 TS 编译器能够确定它们的具体类型,也就能够提供类型检查和代码提示。例如,如果你想对 a 进行字符串操作,或者对 b 进行数学运算,TS 编译器就不会提示你这样做是不安全的,因为它们是你期望的类型。而变量 c 的类型仍然是 any,因为你传入了不匹配的参数类型,这时候 TS 编译器会提示你函数的重载签名没有匹配到你的参数组合,也就提醒你可能会出现错误。

  2. 可选参数与默认参数

    • 可选参数必须跟在必须参数后面的例子:

      1
      2
      3
      4
      5
      6
      7
      function greet(name: string, greeting?: string) {
        // name是必须参数,greeting是可选参数
        console.log(greeting ? `${greeting}, ${name}!` : `Hello, ${name}!`);
      }
      
      greet("Alice"); // Hello, Alice!
      greet("Bob", "Hi"); // Hi, Bob!
    • 有默认值的参数可以放在必须参数的前面或后面的例子:

      1
      2
      3
      4
      5
      6
      7
      8
      function add(x: number = 0, y: number) {
        // x是有默认值的参数,y是必须参数
        return x + y;
      }
      
      console.log(add(1, 2)); // 3
      console.log(add(undefined, 3)); // 3
      console.log(add(4)); // 报错,缺少必须参数y
    • 有默认值的参数如果放在必须参数的后面,那么这样函数的签名和可选参数就是一样的:

      1
      2
      3
      4
      5
      6
      7
      8
      function multiply(x: number, y: number = 1) {
        // x是必须参数,y是有默认值的参数
        return x * y;
      }
      
      console.log(multiply(2, 3)); // 6
      console.log(multiply(4)); // 4
      console.log(multiply(5, undefined)); // 5

    这个函数的签名和function multiply(x: number, y?: number)是一样的。

泛型

泛型是一种编程范式,它允许在程序中定义形式类型参数,然后在泛型实例化时使用实际类型参数来替换形式类型参数。泛型可以提高代码的复用性和类型安全性,让程序更灵活和通用。

TS 中的泛型有以下几种应用场景:

  • 泛型函数:可以定义一个函数,它的参数和返回值的类型由一个类型变量来表示,这样就可以适用于不同的类型,而不需要重复编写相同逻辑的函数。例如:

    1
    2
    3
    function identity<T>(arg: T): T {
      return arg;
    }

    这个函数可以接受任何类型的参数,并返回相同类型的值。我们可以在调用时指定泛型参数的实际类型,如identity<string>("hello"),或者让 TS 自动推断类型,如identity(42)

  • 泛型接口:可以定义一个接口,它的属性或方法的类型由一个或多个类型变量来表示,这样就可以定义通用的数据结构或契约。例如:

    1
    2
    3
    4
    interface MyArray<T> extends Array<T> {
      first: T | undefined;
      last: T | undefined;
    }

    这个接口继承了数组接口,并添加了两个属性,它们的类型都是泛型参数 T。我们可以实现这个接口,并指定 T 的实际类型,如class StringArray implements MyArray<string>

  • 泛型类:可以定义一个类,它的属性或方法的类型由一个或多个类型变量来表示,这样就可以创建通用的类。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class GetMin<T> {
      arr: T[] = [];
      add(ele: T) {
        this.arr.push(ele);
      }
      min(): T {
        let min = this.arr[0];
        this.arr.forEach(function (value) {
          if (value < min) {
            min = value;
          }
        });
        return min;
      }
    }

    这个类可以创建一个存储任何类型元素的数组,并提供一个方法返回最小值。我们可以创建这个类的实例,并指定 T 的实际类型,如let gm1 = new GetMin<number>()

  • 泛型约束:可以使用 extends 关键字来限制泛型参数的范围,使之只能是某个类型或其子类型。这样就可以在泛型中使用一些特定的属性或方法。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    interface Point {
      x: number;
      y: number;
    }
    
    function toArray<T extends Point>(a: T, b: T): T[] {
      return [a, b];
    }

    这个函数只能接受 Point 或其子类型作为参数,并返回相同类型的数组。我们不能传入其他类型的参数,如toArray(1, 2)会报错。

高级类型

当然可以,我会给你一些 TS 中高级类型的例子,你可以参考一下:

  • 交叉类型(Intersection Types)的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 定义两个接口
    interface A {
      name: string;
      age: number;
    }
    
    interface B {
      gender: "male" | "female";
      hobby: string;
    }
    
    // 使用交叉类型将两个接口合并为一个类型
    type C = A & B;
    
    // 创建一个C类型的对象
    let c: C = {
      name: "Tom",
      age: 20,
      gender: "male",
      hobby: "basketball",
    };
  • 联合类型(Union Types)的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 定义一个联合类型,表示一个值可以是number或string
    type D = number | string;
    
    // 使用联合类型作为函数参数的类型
    function print(d: D) {
      // 使用typeof类型保护来判断d的具体类型
      if (typeof d === "number") {
        console.log("The number is " + d);
      } else if (typeof d === "string") {
        console.log("The string is " + d);
      }
    }
    
    // 调用函数,传入不同类型的值
    print(10); // The number is 10
    print("Hello"); // The string is Hello
  • 字面量类型(Literal Types)的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 定义一个字符串字面量类型,表示一个值只能是'hello'或'world'
    type E = "hello" | "world";
    
    // 定义一个数字字面量类型,表示一个值只能是1或2
    type F = 1 | 2;
    
    // 定义一个布尔字面量类型,表示一个值只能是true
    type G = true;
    
    // 定义一个模板字面量类型,表示一个值只能是'Hello ${string}'
    type H = `Hello ${string}`;
    
    // 使用字面量类型创建变量
    let e: E = "hello"; // OK
    let f: F = 2; // OK
    let g: G = true; // OK
    let h: H = `Hello world`; // OK
  • 索引类型(Indexed Types)的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // 定义一个接口
    interface I {
      name: string;
      age: number;
      gender: "male" | "female";
    }
    
    // 使用索引类型查询操作符得到I的所有属性名的类型
    type J = keyof I; // J = 'name' | 'age' | 'gender'
    
    // 使用索引访问操作符得到I的某个属性的类型
    type K = I["name"]; // K = string
    
    // 使用泛型约束和索引类型实现一个获取对象属性值的函数
    function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
      return obj[key];
    }
    
    // 创建一个I类型的对象
    let i: I = {
      name: "Alice",
      age: 18,
      gender: "female",
    };
    
    // 调用函数,传入对象和属性名
    let name = getProp(i, "name"); // name的类型是string,值是'Alice'
    let age = getProp(i, "age"); // age的类型是number,值是18
  • 映射类型(Mapped Types)的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    // 定义一个接口
    interface L {
      name: string;
      age: number;
      gender: "male" | "female";
    }
    
    // 使用映射类型将L的所有属性变为可选的
    type M = {
      [P in keyof L]?: L[P];
    };
    
    // 使用映射类型将L的所有属性变为只读的
    type N = {
      readonly [P in keyof L]: L[P];
    };
    
    // 使用内置的映射类型Pick从L中选择部分属性组成一个新的类型
    type O = Pick<L, "name" | "gender">;
    
    // 使用映射类型创建变量
    let m: M = {
      name: "Bob",
    }; // OK,只有name属性,其他属性可选
    
    let n: N = {
      name: "Bob",
      age: 20,
      gender: "male",
    }; // OK,所有属性只读
    
    n.name = "Tom"; // Error,不能修改只读属性
    
    let o: O = {
      name: "Bob",
      gender: "male",
    }; // OK,只有name和gender属性,其他属性不存在
  • 条件类型(Conditional Types):

    1
    T extends U ? X : Y

    上面的意思就是,如果 T 是 U 的子集,就是类型 X,否则为类型 Y

  • 类型别名(Type Aliases)

    类型别名是一种给一个类型起一个新的名字的方式,它可以让你更方便地引用这个类型,或者给这个类型添加一些语义。类型别名使用 type 关键字来定义,例如 type Name = string 表示给 string 类型起了一个别名叫 Name ,之后你就可以用 Name 来代替 string 了。

    其次,你需要知道什么是泛型(Generics)。泛型是一种在定义类型时使用一个或多个类型参数的方式,它可以让你创建一些适用于任意类型的通用类型,而不是限定于某个具体的类型。泛型使用 <T> 这样的语法来表示类型参数,其中 T 是一个占位符,可以用任何合法的标识符来替换。例如 Array<T> 表示一个泛型数组类型,它可以存放任意类型的元素,例如 Array<number> 表示一个数字数组, Array<string> 表示一个字符串数组。

    那么,类型别名可以是泛型吗?答案是肯定的,类型别名可以使用泛型来定义一些通用的类型,这样就可以让类型别名更灵活和复用。例如,你给出的这个类型别名:

    1
    type Container<T> = { value: T };

    它就是一个泛型类型别名,它表示一个容器类型,它有一个属性叫 value ,这个属性的类型是由类型参数 T 决定的。这样,你就可以用这个类型别名来创建不同类型的容器,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    // 创建一个容器,它的value属性是一个数字
    let numContainer: Container<number> = { value: 10 };
    
    // 创建一个容器,它的value属性是一个字符串
    let strContainer: Container<string> = { value: "Hello" };
    
    // 创建一个容器,它的value属性是一个布尔值
    let boolContainer: Container<boolean> = { value: true };

    这些容器都是使用同一个类型别名 Container<T> 来定义的,只是类型参数 T 不同而已。这样,你就可以用一个类型别名来表示多种可能性,而不需要为每种情况都定义一个新的类型。

    查看 keyof 与 infer 关键字的介绍
    1. inferTypeScript 2.8 中引入的一个关键字,它可以用在条件类型的 extends 子句中,用来推断某个类型变量的具体类型。infer 的作用是在真实分支中引用此推断类型变量,从而获取待推断的类型。infer 可以用在多种场合,例如:

      • 推断函数的参数和返回值类型

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        // 定义一个类型,用来获取函数的参数类型
        type ParamType<T> = T extends (...args: infer P) => any ? P : never;
        
        // 定义一个类型,用来获取函数的返回值类型
        type ReturnType<T> = T extends (...args: any) => infer R ? R : never;
        
        // 测试
        type Fn = (a: number, b: string) => number;
        
        type P = ParamType<Fn>; // [number, string]
        type R = ReturnType<Fn>; // number
      • 推断对象的属性类型

        1
        2
        3
        4
        5
        6
        7
        8
        // 定义一个类型,用来获取对象的属性类型
        type PropType<T, K extends keyof T> = T[K] extends infer U ? U : never;
        
        // 测试
        type Obj = { a: string; b: number };
        
        type A = PropType<Obj, "a">; // string
        type B = PropType<Obj, "b">; // number
      • 推断联合类型的成员类型

        1
        2
        3
        4
        5
        // 定义一个类型,用来获取联合类型的成员类型
        type UnionType<T> = T extends infer U ? U : never;
        
        // 测试
        type U = UnionType<string | number>; // string | number
    2. keyofTypeScript 中的一个关键字,它可以从一个对象类型中提取出它的所有属性名,组成一个字符串或数字的字面量联合类型。例如:

      1
      2
      3
      4
      5
      6
      7
      type Person = {
        name: string;
        age: number;
        gender: string;
      };
      
      type P = keyof Person; // "name" | "age" | "gender"

      keyof 的作用是可以让我们在类型层面上操作对象的属性,比如限制访问对象的某些属性,或者映射对象的属性类型。keyof 也可以和泛型、索引类型、映射类型等一起使用,实现更多的类型操作。下面是一些 keyof 的常见用法:

      • 使用[]访问对象属性的类型:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        type Person = {
          name: string;
          age: number;
          gender: string;
        };
        
        type Name = Person["name"]; // string
        type Age = Person["age"]; // number
        type Gender = Person["gender"]; // string
      • 使用extends限制泛型参数的属性:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        type Person = {
          name: string;
          age: number;
          gender: string;
        };
        
        function getProperty<T, K extends keyof T>(obj: T, key: K) {
          return obj[key];
        }
        
        let p: Person = { name: "Alice", age: 20, gender: "female" };
        
        let name = getProperty(p, "name"); // string
        let age = getProperty(p, "age"); // number
        let gender = getProperty(p, "gender"); // string
        // let hobby = getProperty(p, "hobby"); // Error: Type '"hobby"' is not assignable to type '"name" | "age" | "gender"'
      • 使用in遍历对象的属性:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        type Person = {
          name: string;
          age: number;
          gender: string;
        };
        
        type Keys = keyof Person; // "name" | "age" | "gender"
        
        type PersonRecord = {
          [P in Keys]: string;
        };
        
        // 等价于
        // type PersonRecord = {
        //   name: string;
        //   age: string;
        //   gender: string;
        // };