进阶 · TypeScript 入门教程

    1. 知识点概述
      1. 字符串字面量类型
      2. 元组
      3. 枚举
      4. 类与接口
      5. 泛型
      6. 声明合并
    2. 类型别名
      1. type ... = ...
      2. 使用 type 在创建类型别名
      3. 类型别名用来给一个类型起个新名字
      4. 类型别名常与联合类型配合使用
    1. type StringOrNumber = string | number;
    2. let myVar: StringOrNumber;
    3. // ok
    4. myVar = 'Hello';
    5. // ok
    6. myVar = 123;
    7. // error
    8. myVar = true;
    1. type Name = string;
    2. type NameResolver = () => string;
    3. type NameOrResolver = Name | NameResolver;
    4. function getName(n: NameOrResolver): Name {
    5. if (typeof n === 'string') {
    6. return n;
    7. } else {
    8. return n();
    9. }
    10. }
    1. 字面量类型
    1. let a1: 1 | 2
    2. // ok
    3. a1 = 1
    4. // ok
    5. a1 = 2
    6. // error
    7. a1 = 3
    8. type t2 = '1' | '2'
    9. let a2: t2
    10. // ok
    11. a2 = '1'
    12. // ok
    13. a2 = '2'
    14. // error
    15. a2 = 1
    1. 类(Class):定义了一件事物的抽象特点,包含它的属性和方法
    2. 对象(Object):类的实例,通过 new 生成
    3. 面向对象(OOP)的三大特性:封装、继承、多态
    4. 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据
    5. 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
    6. 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 Cat 和 Dog 都继承自 Animal,但是分别实现了自己的 eat 方法。此时针对某一个实例,我们无需了解它是 Cat 还是 Dog,就可以直接调用 eat 方法,程序会自动判断出来应该如何执行 eat
    7. 存取器(getter & setter):用以改变属性的读取和赋值行为
    8. 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如 public 表示公有属性或方法
    9. 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现
    10. 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口
    11. 在 js 中,类相关的基础知识点回顾
    1. class Animal {
    2. name;
    3. constructor(name) {
    4. this.name = name;
    5. }
    6. sayHi() {
    7. return `My name is ${this.name}`;
    8. }
    9. }
    10. let a = new Animal('Jack');
    11. console.log(a.sayHi()); // My name is Jack
    1. class Cat extends Animal {
    2. constructor(name) {
    3. super(name); // 调用父类的 constructor(name)
    4. console.log(this.name);
    5. }
    6. sayHi() {
    7. return 'Meow, ' + super.sayHi(); // 调用父类的 sayHi()
    8. }
    9. }
    10. let c = new Cat('Tom'); // Tom
    11. console.log(c.sayHi()); // Meow, My name is Tom
    1. class Animal {
    2. constructor(name) {
    3. this.name = name;
    4. }
    5. get name() {
    6. return 'Jack';
    7. }
    8. set name(value) {
    9. console.log('setter: ' + value);
    10. }
    11. }
    12. let a = new Animal('Kitty'); // setter: Kitty
    13. a.name = 'Tom'; // setter: Tom
    14. console.log(a.name); // Jack
    1. class Animal {
    2. static isAnimal(a) {
    3. return a instanceof Animal;
    4. }
    5. }
    6. let a = new Animal('Jack');
    7. Animal.isAnimal(a); // true
    8. a.isAnimal(a); // TypeError: a.isAnimal is not a function
    1. class Animal {
    2. name = 'Jack';
    3. constructor() {
    4. // ...
    5. }
    6. }
    7. let a = new Animal();
    8. console.log(a.name); // Jack
    1. class Animal {
    2. static num = 42;
    3. constructor() {
    4. // ...
    5. }
    6. }
    7. console.log(Animal.num); // 42
    1. ts 中的 3 种访问修饰符(Access Modifiers)
      1. **public** 公有的,可以在任何地方被访问到,默认所有的属性和方法都是 **public**
      2. **private** 私有的,不能在声明它的类的外部访问
      3. **protected** 受保护的,它和 **private** 类似,区别是它在子类中也是允许被访问的
    2. 访问修饰符是 ts 中新增的,在最终的编译结果中是不存在的,仅仅是在我们编写 ts 代码时提供一些约束
    1. class Animal {
    2. public name;
    3. public constructor(name) {
    4. this.name = name;
    5. }
    6. }
    7. let a = new Animal("Jack");
    8. console.log(a.name); // Jack
    9. a.name = "Tom";
    10. console.log(a.name); // Tom
    1. class Animal {
    2. private name;
    3. public constructor(name) {
    4. this.name = name;
    5. }
    6. }
    7. let a = new Animal("Jack");
    8. // error 属性“name”为私有属性,只能在类“Animal”中访问。
    9. console.log(a.name);
    10. // error 属性“name”为私有属性,只能在类“Animal”中访问。
    11. a.name = "Tom";
    1. // private
    2. class Animal {
    3. private name;
    4. public constructor(name) {
    5. this.name = name;
    6. }
    7. }
    8. class Cat extends Animal {
    9. constructor(name) {
    10. super(name);
    11. // error 属性“name”为私有属性,只能在类“Animal”中访问。
    12. console.log(this.name);
    13. }
    14. }
    15. // protected
    16. class Animal {
    17. protected name;
    18. public constructor(name) {
    19. this.name = name;
    20. }
    21. }
    22. class Cat extends Animal {
    23. constructor(name) {
    24. super(name);
    25. // ok
    26. console.log(this.name);
    27. }
    28. }
    1. 当构造函数修饰为 **private** 时,该类不允许被继承或者实例化
    2. 当构造函数修饰为 **protected** 时,该类只允许被继承,但是不允许被实例化
    1. // private
    2. class Animal {
    3. public name;
    4. private constructor(name) {
    5. this.name = name;
    6. }
    7. }
    8. // error 无法扩展类“Animal”。类构造函数标记为私有。
    9. class Cat extends Animal {
    10. constructor(name) {
    11. super(name);
    12. }
    13. }
    14. // error 类“Animal”的构造函数是私有的,仅可在类声明中访问。
    15. let a = new Animal("Jack");
    16. // protected
    17. class Animal {
    18. public name;
    19. protected constructor(name) {
    20. this.name = name;
    21. }
    22. }
    23. // ok
    24. class Cat extends Animal {
    25. constructor(name) {
    26. super(name);
    27. }
    28. }
    29. // error 类“Animal”的构造函数是受保护的,仅可在类声明中访问。
    30. let a = new Animal("Jack");
    1. 访问修饰符也可作用于构造函数的参数,等同于在类中定义该属性并给该属性赋值
    1. class Animal {
    2. constructor(public name: string) {}
    3. }
    4. let a = new Animal("Jack");
    5. console.log(a.name); // Jack
    6. // 等效
    7. class Animal {
    8. name: string;
    9. constructor(name: string) {
    10. this.name = name;
    11. }
    12. }
    13. let a = new Animal("Jack");
    14. console.log(a.name); // Jack
    1. **readonly** 可以在类中修饰只读成员
    2. **readonly** 若和其他访问修饰符同时存在,需要将 **readonly** 写在后面
    3. **readonly** 提供了一种简洁的语法来确保类的某些属性在创建实例后不可更改,这有助于增加代码的不变性和可预测性。
    1. class Circle {
    2. readonly PI = 3.14159;
    3. constructor(public readonly radius: number) {}
    4. getArea() {
    5. return this.PI * this.radius * this.radius;
    6. }
    7. }
    8. const circle = new Circle(10);
    9. console.log(circle.PI) // 3.14159
    10. console.log(circle.radius) // 10
    11. // error 无法为“PI”赋值,因为它是只读属性。
    12. circle.PI = 3.14;
    1. class Point {
    2. constructor(readonly x: number, readonly y: number) {}
    3. }
    4. const point = new Point(5, 10);
    5. console.log(point.x) // 5
    6. console.log(point.y) // 10
    7. // error 无法为“x”赋值,因为它是只读属性。
    8. point.x = 20;
    1. 你可以将 **readonly****private** 一起使用,这意味着这个属性只能在类内部读取,且不能修改
    1. class Counter {
    2. // 在类内部只读
    3. private readonly maxCount = 100;
    4. // 在类内部可读可写
    5. private _count = 0;
    6. increment() {
    7. if (this._count < this.maxCount) {
    8. this._count++;
    9. } else {
    10. console.log("Reached max count!");
    11. }
    12. }
    13. getCount() {
    14. return this._count;
    15. }
    16. }
    1. 抽象类(Abstract Class)是 TypeScript(以及其他一些面向对象语言)中的一个重要概念,用于定义基础类的结构和部分实现,而不允许直接实例化。
    2. 抽象类是为继承而设计的,通常作为其他派生类的基类使用。
    3. 抽象类可以包含具体成员(已实现的方法和属性)以及抽象成员(没有具体实现的方法和属性)。
    4. 抽象类中的抽象方法必须被子类实现。
    5. 抽象类为建立一系列具有某些相似性的类提供了一个基础,使你可以共享某些功能,同时确保派生类遵循某些结构。
    6. 在 ts 中,使用 **abstract** 来定义抽象类和其中的抽象成员
    1. abstract class Animal {
    2. abstract makeSound(): void; // 抽象方法,没有具体实现
    3. move(): void {
    4. // 普通方法,有具体实现
    5. console.log("The animal moves.");
    6. }
    7. }
    8. class Dog extends Animal {
    9. makeSound(): void {
    10. console.log("Woof!");
    11. }
    12. }
    13. class Cat extends Animal {
    14. makeSound(): void {
    15. console.log("Meow!");
    16. }
    17. }
    18. // error 无法创建抽象类的实例。
    19. // let animal = new Animal();
    20. let dog = new Dog();
    21. dog.makeSound(); // Woof!
    22. dog.move(); // The animal moves.
    23. let cat = new Cat();
    24. cat.makeSound(); // Meow!
    1. 在 ts 中,类有两个主要角色:
      1. 工厂角色:当你使用 new ClassName() 这样的语法时,你实际上是在用类作为一个工厂来创建新的实例对象。
      2. 类型角色:当你在变量、函数参数或函数返回值上使用类名作为类型标注时,你实际上是在用类作为一个类型。这意味着你可以用这个类名来限制赋值给该变量的值的类型、函数参数的类型或函数返回的类型。
    1. class Dog {
    2. name: string;
    3. constructor(name: string) {
    4. this.name = name;
    5. }
    6. bark(): string {
    7. return `${this.name} says woof!`;
    8. }
    9. }
    10. // 1. 使用 Dog 作为工厂创建新实例
    11. const myDog = new Dog("Rex");
    12. // 2. 使用 Dog 作为类型来限制函数参数的类型
    13. function printDogName(d: Dog) {
    14. console.log(d.name);
    15. }
    16. // ok
    17. printDogName(myDog); // Rex
    1. 实现(implements)是面向对象中的一个重要概念。
    2. 一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 **implements** 关键字来实现。这个特性大大提高了面向对象的灵活性。
    3. 在 ts(和很多其他面向对象的编程语言)中,当我们说一个类“实现”一个接口,我们是指这个类遵循了该接口定义的规范或结构。在实际操作中,这意味着类包含了接口中定义的所有属性和方法,并保证了它们的类型正确性。
    4. 一个类可以实现一个或多个接口。
    // 定义一个接口,表示有名字和一个打招呼的功能
    interface Greetable {
      name: string;
      greet(): void;
    }
    
    // 定义一个类,它“实现”了上述接口
    class Person implements Greetable {
      name: string;
    
      constructor(name: string) {
        this.name = name;
      }
    
      greet() {
        console.log(`Hello, my name is ${this.name}.`);
      }
    }
    
    // 使用
    const person = new Person("Alice");
    person.greet(); // Hello, my name is Alice.
    
    // 报警接口
    interface Alarm {
      alert(): void;
    }
    
    // 车灯接口
    interface Light {
      lightOn(): void;
      lightOff(): void;
    }
    
    // 汽车类实现“报警接口”和“车灯接口”
    class Car implements Alarm, Light {
      alert() {
        console.log("Car alert");
      }
      lightOn() {
        console.log("Car light on");
      }
      lightOff() {
        console.log("Car light off");
      }
    }
    
    1. 常见的面向对象语言中,接口是不能继承类的,但是在 ts 中,类也可以当做一个类型来使用,因此 ts 是允许接口继承类的
    2. 在接口继承类的时候,也只会继承它的实例属性和实例方法。
    class Point {
      x: number;
      y: number;
      constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
      }
    }
    
    interface Point3d extends Point {
      z: number;
    }
    
    let point3d: Point3d = { x: 1, y: 2, z: 3 };
    
    class Point {
      /** 静态属性,坐标系原点 */
      static origin = new Point(0, 0);
      /** 静态方法,计算与原点距离 */
      static distanceToOrigin(p: Point) {
        return Math.sqrt(p.x * p.x + p.y * p.y);
      }
      /** 实例属性,x 轴的值 */
      x: number;
      /** 实例属性,y 轴的值 */
      y: number;
      /** 构造函数 */
      constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
      }
      /** 实例方法,打印此点 */
      printPoint() {
        console.log(this.x, this.y);
      }
    }
    
    interface PointInstanceType {
      x: number;
      y: number;
      printPoint(): void;
    }
    
    let p1: Point;
    let p2: PointInstanceType;
    
    // 上例中最后的类型 Point 和类型 PointInstanceType 是等价的。
    
    1. 泛型(Generics)
    2. 泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
    3. 泛型提供了一种方法,让代码能够以类型安全的方式在多种数据类型上工作,而不是被限制于一个单一数据类型。
    4. 泛型可以被视为类型变量,它们用于捕获和重复使用函数或类的参数类型。
    5. 思考:如何编写一个函数,使其能够接受多种类型的参数,并返回与输入类型相同的类型?
      1. 在上述描述中,类型是变化的,而非写死的,对于类似这种类型会变化的情况,可以使用泛型来解决
      2. 泛型其实就是类型变量
    6. 泛型提供了在编译时检查类型的能力,而不是运行时。
    7. 通过使用泛型,您可以编写更通用、可重用的函数和类,而不是为每种数据类型重写它们。
    8. 您可以为泛型定义约束,以确保输入遵循特定的形状或方法。
    function identity<T>(arg: T): T {
      return arg;
    }
    
    let output1 = identity<string>("myString");
    // ts 能够推断出 output1: string
    
    let output2 = identity<number>(42);
    // ts 能够推断出 output2: number
    
    function createArray<T>(length: number, value: T): Array<T> {
      let result: T[] = [];
      for (let i = 0; i < length; i++) {
        result[i] = value;
      }
      return result;
    }
    
    let arr1 = createArray<string>(3, "x"); // ['x', 'x', 'x']
    // ts 能够推断出 arr1: string[]
    
    let arr2 = createArray<string>(3, "x"); // ['x', 'x', 'x']
    // ts 能够推断出 arr2: string[]
    
    1. 当定义泛型时,你可以定义多个类型参数。多个类型参数之间用逗号 , 分隔。
    function swap<T, U>(tuple: [T, U]): [U, T] {
      return [tuple[1], tuple[0]];
    }
    
    const result = swap([7, "seven"]); // ['seven', 7]
    // ts 能够推断出 result: [string, number]
    
    1. 在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法。这时,你可以通过定义一个接口来约束泛型变量的形状(即它应该有哪些属性或方法)。
    // 定义一个接口,约束了泛型 T 必须有 length 属性
    interface HasLength {
      length: number;
    }
    
    function printLength<T extends HasLength>(input: T): void {
      console.log(input.length);
      // 现在我们可以安全地访问 length 属性,因为我们约束了 T 必须有 length 属性
    }
    
    printLength("Hello"); // 输出:5
    printLength([1, 2, 3, 4, 5]); // 输出:5
    
    // printLength(123);
    // error 类型“number”的参数不能赋给类型“HasLength”的参数。
    // 因为 number 类型没有 length 属性
    
    1. 多个类型参数之间也可以互相约束对
    function copyFields<T extends U, U>(target: T, source: U): T {
      for (let id in source) {
        target[id] = (<T>source)[id];
      }
      return target;
    }
    
    let x = { a: 1, b: 2, c: 3, d: 4 };
    
    let result = copyFields(x, { b: 10, d: 20 });
    
    console.log(result);
    // { a: 1, b: 10, c: 3, d: 20 }
    
    1. 泛型接口允许你在定义接口时同时定义一个或多个类型参数
    interface GenericIdentityFn<T> {
      (arg: T): T;
    }
    
    function identity<T>(arg: T): T {
      return arg;
    }
    
    let myIdentity: GenericIdentityFn<number> = identity;
    console.log(myIdentity(100)); // 100
    
    interface GenericDictionary<T> {
      [key: string]: T;
    }
    
    let stringDict: GenericDictionary<string> = {
      first: "Hello",
      second: "World",
    };
    
    console.log(stringDict["first"]); // Hello
    
    interface Pair<T, U> {
      first: T;
      second: U;
    }
    
    let pair: Pair<number, string> = {
      first: 1,
      second: "one",
    };
    
    console.log(pair.first); // 1
    console.log(pair.second); // one
    
    1. 泛型类与泛型接口类似
    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;
    };
    
    interface Lengthwise {
      length: number;
    }
    
    class Box<T extends Lengthwise> {
      content: T;
    
      constructor(content: T) {
        this.content = content;
      }
    
      printLength() {
        console.log(this.content.length);
      }
    }
    
    let box1 = new Box("Hello, World!");
    box1.printLength(); // 13
    
    let box2 = new Box([1, 2, 3, 4, 5]);
    box2.printLength(); // 5
    
    1. 泛型参数的默认值
    function createArray<T = string>(length: number, value: T): Array<T> {
      let result: T[] = [];
      for (let i = 0; i < length; i++) {
        result[i] = value;
      }
      return result;
    }
    
    // 在这个例子中,T 的默认类型是 number
    class MyArray<T = number> {
      private items: T[] = [];
    
      add(item: T): void {
        this.items.push(item);
      }
    
      getItems(): T[] {
        return this.items;
      }
    }
    
    let numberArray = new MyArray(); // 使用默认的 number 类型
    numberArray.add(1);
    numberArray.add(2);
    console.log(numberArray.getItems()); // [1, 2]
    
    let stringArray = new MyArray<string>(); // 明确指定为 string 类型
    stringArray.add("Hello");
    stringArray.add("World");
    console.log(stringArray.getItems()); // ["Hello", "World"]
    
    // 使用另一个类型
    let booleanArray = new MyArray<boolean>();
    booleanArray.add(true);
    booleanArray.add(false);
    console.log(booleanArray.getItems()); // [true, false]
    
    1. 声明合并
      1. 如果定义了两个相同名字的函数、接口或类,那么它们会合并成一个类型
      2. 函数:可以使用重载定义多个函数类型
      3. 接口:接口中的属性在合并时会简单的合并到一个接口中
        1. 同名字段合并时,需要注意类型兼容性
    // 约束函数被调用的方式 1
    function combine(input1: number, input2: number): number;
    // 约束函数被调用的方式 2
    function combine(input1: string, input2: string): string;
    function combine(input1: number | string, input2: number | string) {
      if (typeof input1 === "number" && typeof input2 === "number") {
        return input1 + input2;
      } else if (typeof input1 === "string" && typeof input2 === "string") {
        return input1.concat(input2);
      }
    }
    
    // ok
    combine(1, 2) // 3
    
    // ok
    combine('1', '2') // 12
    
    interface Alarm {
      price: number;
    }
    interface Alarm {
      weight: number;
    }
    
    // 等效
    
    interface Alarm {
      price: number;
      weight: number;
    }
    
    1. ts
    2. ts
    3. ts
    4. ts
    5. ts
    6. ts
    7. ts
    8. ts
    9. ts
    10. ts
    11. ts
    12. ts
    13. ts
    14. ts
    15. ts
    16. ts
    17. ts
    18. ts