TypeScript手册
TypeScript手册

一、TypeScript 简介

TypeScript 是由微软开发的一种开源编程语言,是 JavaScript 的一个超集。它增加了静态类型和类型检查,使得 JavaScript 的开发更为安全和高效。TypeScript 的设计目标是改善 JavaScript 的开发体验,尤其是在大型代码库和复杂应用程序中。

为什么使用 TypeScript?
  1. 类型安全:TypeScript 提供了静态类型检查,可以在编译阶段捕捉到许多潜在的错误,减少运行时错误的可能性。

  2. 增强的开发工具支持:TypeScript 的类型系统使得 IDE 可以提供更智能的代码补全、导航和重构功能,提升开发效率。

  3. 现代 JavaScript 特性:TypeScript 支持 ES6+ 的所有特性,并且可以通过编译选项将代码转换为兼容旧版本 JavaScript 的代码,确保在各种浏览器和环境中的兼容性。

  4. 渐进式采用:TypeScript 可以逐步引入到现有的 JavaScript 项目中。可以在项目中逐步添加 TypeScript 文件,并且逐步利用 TypeScript 的类型检查功能。

TypeScript 的核心概念
  • 类型:TypeScript 引入了类型系统,允许定义变量、函数参数和返回值的类型,从而帮助在开发阶段检测类型错误。

  • 接口:接口用于定义对象的结构,可以确保对象符合预期的形状和约定。

  • 泛型:泛型允许编写能够处理多种数据类型的函数和类,提高代码的重用性和灵活性。

  • 模块:TypeScript 支持 ES6 模块系统,允许将代码拆分为多个模块,促进代码组织和管理。

  • 装饰器:装饰器是 TypeScript 的一个实验性特性,允许通过元编程的方式修改类的行为和属性。

TypeScript 的编译器将 TypeScript 代码转换为 JavaScript 代码,支持多种目标 JavaScript 版本,确保生成的代码可以在不同的环境中运行。

二、基础类型

TypeScript 引入了一些新的基本类型,并增强了 JavaScript 的类型系统。这些基础类型帮助开发者在编写代码时明确数据的结构和类型,从而减少错误和提高代码的可维护性。

1. 原始类型
  • boolean:表示逻辑值 truefalse

    let isActive: boolean = true;
    
  • number:表示所有数字,包括整数和浮点数。TypeScript 不区分整数和浮点数,所有数字都使用 number 类型。

    let age: number = 30;
    let price: number = 19.99;
    
  • string:表示文本数据,包括字符和字符串。支持单引号、双引号和反引号(模板字符串)。

    let name: string = "Alice";
    let greeting: string = `Hello, ${name}`;
    
  • undefined:表示一个变量尚未被赋值。默认情况下,所有未初始化的变量都具有 undefined 值。

    let notDefined: undefined;
    
  • null:表示一个变量的值为空或无效。null 通常与 undefined 一起使用。

    let emptyValue: null = null;
    
2. 特殊类型
  • any:表示任何类型。使用 any 可以绕过类型检查,但过度使用 any 会削弱 TypeScript 的类型安全性。

    let randomValue: any = 5;
    randomValue = "A string now";
    
  • void:表示函数没有返回值。通常用于函数的返回类型,以标识该函数不会返回任何有用的结果。

    function logMessage(message: string): void {
      console.log(message);
    }
    
  • never:表示永远不会发生的值。通常用于函数的返回类型,表示该函数不会正常结束(例如,抛出异常)。

    function throwError(message: string): never {
      throw new Error(message);
    }
    
3. 数组和元组
  • 数组:TypeScript 中的数组可以使用 类型[]Array<类型> 表示。

    let numbers: number[] = [1, 2, 3];
    let strings: Array<string> = ["apple", "banana"];
    
  • 元组:元组是一种数组类型,允许定义一个固定大小的数组,每个元素可以是不同的类型。

    let tuple: [string, number] = ["Alice", 30];
    
4. 枚举
  • 枚举:用于定义一组命名的常数。枚举类型可以帮助在代码中使用更具可读性的名称代替数字或字符串。
    enum Direction {
      Up,
      Down,
      Left,
      Right
    }
    
    let move: Direction = Direction.Up;
    

这些基础类型构成了 TypeScript 类型系统的核心部分,通过这些类型,开发者可以更准确地描述和控制数据的结构和类型。

三、TypeScript 变量声明

在 TypeScript 中,变量声明的方式与 JavaScript 类似,但 TypeScript 增强了变量声明的类型安全性。可以使用 letconstvar 关键字声明变量,并且可以显式指定变量的类型或让 TypeScript 自动推断类型。

1. 使用 letconst
  • let:用于声明可以被重新赋值的变量。它具有块级作用域,即变量只在其声明的代码块内有效。

    let count: number = 10;
    count = 20; // 可以重新赋值
    
  • const:用于声明常量。声明的变量不可重新赋值,但对于对象和数组,const 只保证引用不变,数组的元素或对象的属性依然可以改变。

    const pi: number = 3.14;
    // pi = 3.14159; // 错误:不能重新赋值
    
    const numbers: number[] = [1, 2, 3];
    numbers.push(4); // 合法:数组元素可以改变
    
2. 使用 var
  • varvar 是 ES5 引入的变量声明方式,具有函数作用域而不是块级作用域。虽然 var 在现代 JavaScript 中仍然有效,但推荐使用 letconst 以获得更好的块级作用域支持和减少潜在错误。
    var message: string = "Hello";
    if (true) {
      var message: string = "World"; // 在整个函数作用域内有效
    }
    console.log(message); // 输出 "World"
    
3. 类型注解

在 TypeScript 中,可以显式地为变量指定类型。这有助于 TypeScript 在编译阶段检查类型错误。

  • 基本类型注解

    let name: string = "Alice";
    let age: number = 25;
    let isStudent: boolean = true;
    
  • 数组和对象类型注解

    let numbers: number[] = [1, 2, 3];
    let person: { name: string; age: number } = { name: "Alice", age: 25 };
    
  • 函数返回值类型注解

    function greet(name: string): string {
      return `Hello, ${name}`;
    }
    
4. 类型推断

TypeScript 也可以通过类型推断来自动推断变量的类型。当没有显式指定类型时,TypeScript 会根据变量的初始值推断其类型。

  • 类型推断示例
    let message = "Hello"; // TypeScript 推断 message 为 string 类型
    message = "World"; // 合法
    // message = 123; // 错误:不能将 number 赋值给 string 类型
    

类型注解和类型推断使得 TypeScript 能够在编译阶段捕捉到类型错误。使用 letconst 关键字可以更好地控制变量的作用域和变更。

四、接口

接口(interface)是 TypeScript 中一种用于定义对象结构的强大工具。它允许定义对象的形状、结构、以及约定,使得代码更加清晰和类型安全。接口可以用于定义类的结构、函数的参数和返回类型,以及对象的类型。

1. 定义接口

接口用于定义对象的结构,包括对象的属性和方法。

  • 基础接口

    interface Person {
      name: string;
      age: number;
    }
    
    const person: Person = {
      name: "Alice",
      age: 30
    };
    
  • 可选属性:接口中的属性可以是可选的,用 ? 符号标记。

    interface Person {
      name: string;
      age?: number; // age 是可选的
    }
    
    const person1: Person = { name: "Bob" };
    const person2: Person = { name: "Charlie", age: 25 };
    
  • 只读属性:使用 readonly 修饰符来表示属性是只读的,不能被修改。

    interface Person {
      readonly id: number;
      name: string;
    }
    
    const person: Person = { id: 1, name: "Daisy" };
    // person.id = 2; // 错误:不能修改只读属性
    
2. 接口与函数

接口可以用于定义函数的结构,包括函数的参数和返回类型。

  • 函数类型接口

    interface Add {
      (a: number, b: number): number;
    }
    
    const add: Add = (a, b) => a + b;
    
  • 可选参数:接口定义的函数参数可以是可选的。

    interface Greet {
      (name: string, greeting?: string): string;
    }
    
    const greet: Greet = (name, greeting = "Hello") => `${greeting}, ${name}`;
    

3. 接口与类

接口可以用于定义类的结构,确保类实现了指定的属性和方法。

  • 类实现接口
    interface Shape {
      width: number;
      height: number;
      getArea(): number;
    }
    
    class Rectangle implements Shape {
      width: number;
      height: number;
    
      constructor(width: number, height: number) {
        this.width = width;
        this.height = height;
      }
    
      getArea(): number {
        return this.width * this.height;
      }
    }
    
    const rect = new Rectangle(10, 20);
    console.log(rect.getArea()); // 输出 200
    
4. 继承接口

接口可以继承其他接口,允许创建复杂的接口结构。

  • 接口继承
    interface Person {
      name: string;
      age: number;
    }
    
    interface Employee extends Person {
      employeeId: number;
    }
    
    const employee: Employee = {
      name: "Eve",
      age: 28,
      employeeId: 12345
    };
    
5. 接口的索引签名

接口可以使用索引签名来描述具有动态属性的对象。

  • 索引签名
    interface StringDictionary {
      [key: string]: string;
    }
    
    const dictionary: StringDictionary = {
      "hello": "world",
      "foo": "bar"
    };
    

五、类

类是 TypeScript 中一种面向对象编程的核心概念,用于创建对象和处理对象之间的关系。TypeScript 的类扩展了 JavaScript 的类功能,增加了类型注解、访问修饰符和其他特性。

1. 定义类

一个类可以包含属性和方法,用于定义对象的结构和行为。

  • 基本类定义
    class Person {
      name: string;
      age: number;
    
      constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
      }
    
      greet(): string {
        return `Hello, my name is ${this.name}`;
      }
    }
    
    const person = new Person("Alice", 30);
    console.log(person.greet()); // 输出 "Hello, my name is Alice"
    
2. 访问修饰符

TypeScript 提供了访问修饰符来控制类成员的可见性。常用的修饰符包括 publicprivateprotected

  • public:公共成员,默认值,类的外部可以访问。

    class Person {
      public name: string;
    
      constructor(name: string) {
        this.name = name;
      }
    }
    
  • private:私有成员,仅在类的内部可以访问。

    class Person {
      private age: number;
    
      constructor(age: number) {
        this.age = age;
      }
    
      getAge(): number {
        return this.age;
      }
    }
    
    const person = new Person(30);
    // console.log(person.age); // 错误:属性 'age' 是私有的
    console.log(person.getAge()); // 合法:通过公共方法访问私有属性
    
  • protected:受保护成员,类的内部和继承的子类可以访问。

    class Person {
      protected age: number;
    
      constructor(age: number) {
        this.age = age;
      }
    }
    
    class Employee extends Person {
      getAge(): number {
        return this.age; // 合法:子类可以访问受保护属性
      }
    }
    
3. 静态成员

静态成员属于类本身,而不是类的实例。可以通过类名直接访问静态成员。

  • 静态属性和方法
    class MathUtils {
      static PI: number = 3.14;
    
      static calculateCircumference(radius: number): number {
        return 2 * MathUtils.PI * radius;
      }
    }
    
    console.log(MathUtils.PI); // 输出 3.14
    console.log(MathUtils.calculateCircumference(5)); // 输出 31.4
    
4. 继承

TypeScript 支持类的继承,使得可以创建基于现有类的派生类,从而实现代码重用。

  • 类继承
    class Animal {
      name: string;
    
      constructor(name: string) {
        this.name = name;
      }
    
      speak(): string {
        return `${this.name} makes a sound.`;
      }
    }
    
    class Dog extends Animal {
      bark(): string {
        return `${this.name} barks.`;
      }
    }
    
    const dog = new Dog("Buddy");
    console.log(dog.speak()); // 输出 "Buddy makes a sound."
    console.log(dog.bark()); // 输出 "Buddy barks."
    
5. 抽象类

抽象类不能被实例化,用于定义一组必须由子类实现的抽象方法。它们可以包含具体实现的方法。

  • 抽象类和抽象方法
    abstract class Animal {
      abstract makeSound(): string; // 抽象方法,必须在子类中实现
    
      move(): string {
        return "Moving...";
      }
    }
    
    class Dog extends Animal {
      makeSound(): string {
        return "Woof!";
      }
    }
    
    const dog = new Dog();
    console.log(dog.makeSound()); // 输出 "Woof!"
    console.log(dog.move()); // 输出 "Moving..."
    

六、泛型

泛型是 TypeScript 的强大特性之一,允许编写可以处理不同数据类型的代码而不失去类型安全性。泛型提供了一种灵活的方式来创建可重用的组件,函数和类,而不需要在每种类型上都重复编写相似的代码。

1. 泛型函数

泛型函数允许在函数定义时指定类型,并在调用函数时确定具体类型。

  • 基本泛型函数
    function identity<T>(arg: T): T {
      return arg;
    }
    
    const num = identity(5); // num 的类型是 number
    const str = identity("hello"); // str 的类型是 string
    
2. 泛型接口

泛型接口允许定义具有泛型参数的接口,从而在实现时可以指定具体类型。

  • 泛型接口示例
    interface GenericIdentityFn<T> {
      (arg: T): T;
    }
    
    const stringIdentity: GenericIdentityFn<string> = (arg) => arg;
    const numberIdentity: GenericIdentityFn<number> = (arg) => arg;
    
3. 泛型类

泛型类允许在类中使用泛型,以便类可以处理多种不同类型。

  • 泛型类示例
    class Box<T> {
      content: T;
    
      constructor(content: T) {
        this.content = content;
      }
    
      getContent(): T {
        return this.content;
      }
    }
    
    const numberBox = new Box<number>(123);
    const stringBox = new Box<string>("Hello");
    
    console.log(numberBox.getContent()); // 输出 123
    console.log(stringBox.getContent()); // 输出 "Hello"
    
4. 泛型约束

泛型约束允许限制泛型类型的范围,以确保类型满足某些条件。

  • 基本约束示例
    function lengthOfArray<T extends { length: number }>(arg: T): number {
      return arg.length;
    }
    
    console.log(lengthOfArray([1, 2, 3])); // 输出 3
    console.log(lengthOfArray("Hello")); // 输出 5
    
5. 泛型默认值

可以为泛型参数指定默认值,以便在没有指定泛型参数时使用默认类型。

  • 泛型默认值示例
    function wrap<T = string>(value: T): T {
      return value;
    }
    
    const result1 = wrap(123); // 默认使用 string 类型,所以结果是 "123"
    const result2 = wrap("hello"); // 结果是 "hello"
    
6. 多个泛型参数

可以为函数、接口和类指定多个泛型参数,以处理更复杂的类型。

  • 多个泛型参数示例
    function merge<T, U>(obj1: T, obj2: U): T & U {
      return { ...obj1, ...obj2 };
    }
    
    const result = merge({ name: "Alice" }, { age: 30 });
    console.log(result); // 输出 { name: "Alice", age: 30 }
    
7. 泛型与类型推断

TypeScript 能够根据传入的参数自动推断泛型的具体类型。

  • 类型推断示例
    function wrap<T>(value: T): T {
      return value;
    }
    
    const wrappedValue = wrap("test"); // TypeScript 自动推断 T 为 string
    

七、枚举

枚举(enum)是 TypeScript 提供的一种用于定义一组命名常量的方式。枚举使得代码中的常量更加具名和可读,适合用于表示一组相关的值。TypeScript 中的枚举可以是数字枚举、字符串枚举或异构枚举。

1. 数字枚举

数字枚举是最常用的枚举类型,每个枚举成员都会被赋一个数字值,默认从 0 开始递增。也可以手动设置枚举成员的值。

  • 基本数字枚举

    enum Direction {
      Up,
      Down,
      Left,
      Right
    }
    
    let move: Direction = Direction.Up;
    console.log(move); // 输出 0
    
  • 设置初始值:可以手动设置枚举成员的初始值,后续成员会基于这个值自动递增。

    enum Direction {
      Up = 1,
      Down,
      Left,
      Right
    }
    
    console.log(Direction.Up); // 输出 1
    console.log(Direction.Down); // 输出 2
    
  • 设置具体值:也可以为每个枚举成员设置具体的值。

    enum Direction {
      Up = 10,
      Down = 20,
      Left = 30,
      Right = 40
    }
    
    console.log(Direction.Up); // 输出 10
    
2. 字符串枚举

字符串枚举使用字符串值来定义枚举成员。每个成员的值必须是一个字符串字面量。

  • 基本字符串枚举
    enum Direction {
      Up = "UP",
      Down = "DOWN",
      Left = "LEFT",
      Right = "RIGHT"
    }
    
    let move: Direction = Direction.Up;
    console.log(move); // 输出 "UP"
    
3. 异构枚举

异构枚举包含了数字和字符串类型的成员。这种用法不太常见,但在某些场景中可能会有用。

  • 异构枚举示例
    enum MixedEnum {
      No = 0,
      Yes = "YES"
    }
    
    console.log(MixedEnum.No); // 输出 0
    console.log(MixedEnum.Yes); // 输出 "YES"
    
4. 计算属性和常量属性

枚举成员可以是常量(编译时已知的值)或计算得来的值(运行时计算得到的值)。常量枚举成员的值在编译时就确定了,而计算枚举成员的值则在运行时计算。

  • 常量枚举

    enum Shape {
      Circle = 1,
      Square = 2
    }
    
  • 计算属性

    enum Shape {
      Circle = Math.PI,
      Square = 4
    }
    
5. 反向映射

TypeScript 的数字枚举会生成一个反向映射,使得可以通过值获取到对应的枚举成员名。

  • 反向映射示例
    enum Direction {
      Up = 1,
      Down,
      Left,
      Right
    }
    
    console.log(Direction[1]); // 输出 "Up"
    
6. 枚举的使用

枚举在代码中可以提供更具可读性的值,常用于状态码、配置选项等场景。

  • 使用枚举
    function move(direction: Direction): void {
      switch (direction) {
        case Direction.Up:
          console.log("Moving up");
          break;
        case Direction.Down:
          console.log("Moving down");
          break;
        case Direction.Left:
          console.log("Moving left");
          break;
        case Direction.Right:
          console.log("Moving right");
          break;
      }
    }
    
    move(Direction.Left); // 输出 "Moving left"
    

八、类型兼容性

类型兼容性是 TypeScript 中一个重要的概念,它定义了一个类型是否可以被赋值给另一个类型。TypeScript 使用结构性类型系统(也称为鸭子类型)来判断类型兼容性,这意味着类型兼容性是基于类型的结构(成员和方法)而不是名称。

1. 基本类型兼容性

基本类型(如 numberstringboolean)之间的兼容性是基于它们的具体类型和值。

  • 基本类型兼容性
    let num: number = 42;
    let str: string = "hello";
    num = str; // 错误:类型“string”不能赋值给类型“number”
    
2. 对象类型兼容性

对象类型的兼容性是基于对象的结构(即它的属性和方法)。

  • 对象类型兼容性

    interface Person {
      name: string;
      age: number;
    }
    
    let person: Person = { name: "Alice", age: 30 };
    let employee = { name: "Bob", age: 25, position: "Developer" };
    
    person = employee; // 合法:employee 拥有 Person 所需的属性
    

    在上述示例中,employee 对象拥有 Person 类型所需的所有属性,因此可以赋值给 person 变量。类型兼容性基于 employee 包含了 Person 类型所需的属性。

3. 函数类型兼容性

函数类型的兼容性取决于函数的参数和返回值的类型。一个函数可以赋值给另一个函数,如果它的参数和返回值符合预期。

  • 函数类型兼容性

    function log(message: string): void {
      console.log(message);
    }
    
    function print(message: string, times: number): void {
      console.log(message.repeat(times));
    }
    
    log = print; // 错误:函数类型“(message: string, times: number) => void”不可赋值给类型“(message: string) => void”
    

    在上述示例中,print 函数接受两个参数,但 log 函数只接受一个参数,因此 print 不兼容 log

    • 参数协变:函数参数的类型可以更加宽松(子类型),即函数可以接受更多的参数类型。
    • 返回值逆变:函数的返回值类型可以更加严格(父类型),即函数可以返回更少的类型。
4. 结构性类型系统

TypeScript 是一种结构性类型系统,这意味着类型兼容性是基于对象的结构(即它的成员和方法),而不是基于名称或声明。

  • 结构性类型系统

    interface A {
      x: number;
      y: number;
    }
    
    interface B {
      x: number;
      y: number;
      z: number;
    }
    
    let a: A = { x: 1, y: 2 };
    let b: B = { x: 1, y: 2, z: 3 };
    
    a = b; // 合法:B 包含了 A 所需的属性
    

    在这个例子中,b 对象符合 A 类型的结构,因此可以赋值给 a 变量。

5. 类型别名和接口的兼容性

typeinterface 都可以用来定义类型,它们之间的兼容性通常基于它们的结构。

  • 类型别名和接口兼容性
    type Point = { x: number; y: number };
    interface Coordinate {
      x: number;
      y: number;
    }
    
    let point: Point = { x: 1, y: 2 };
    let coordinate: Coordinate = { x: 1, y: 2 };
    
    point = coordinate; // 合法:Coordinate 类型符合 Point 类型的结构
    
6. 交叉类型和联合类型的兼容性

交叉类型(&)和联合类型(|)的兼容性基于它们的结构。

  • 交叉类型兼容性

    type A = { x: number };
    type B = { y: number };
    
    let a: A = { x: 1 };
    let b: B = { y: 2 };
    
    let c: A & B = { x: 1, y: 2 }; // 合法:c 包含了 A 和 B 的所有属性
    
  • 联合类型兼容性

    type A = { x: number };
    type B = { y: number };
    
    let a: A | B = { x: 1 }; // 合法:a 可能是 A 类型或 B 类型
    let b: B = { y: 2 };
    
    a = b; // 合法:a 可能是 B 类型
    

九、高级类型

高级类型是 TypeScript 中用于处理更复杂的类型关系的特性。这些类型可以帮助创建更灵活、强大且类型安全的代码。以下是一些常见的高级类型及其使用方式:

1. 交叉类型(Intersection Types)

交叉类型通过将多个类型组合成一个类型来创建更复杂的类型。交叉类型使用 & 符号来实现。

  • 交叉类型示例

    interface A {
      x: number;
    }
    
    interface B {
      y: number;
    }
    
    type AB = A & B;
    
    const obj: AB = { x: 1, y: 2 }; // obj 需要同时满足 A 和 B 的结构
    

    交叉类型常用于将多个接口或类型合并为一个新的类型,从而实现类型的扩展和组合。

2. 联合类型(Union Types)

联合类型允许一个变量可以是几种不同类型中的一种。联合类型使用 | 符号来定义。

  • 联合类型示例

    type StringOrNumber = string | number;
    
    function print(value: StringOrNumber) {
      console.log(value);
    }
    
    print("Hello"); // 合法
    print(123);     // 合法
    

    联合类型在函数参数和变量的声明中非常有用,允许处理不同类型的值。

3. 类型别名(Type Aliases)

类型别名允许为复杂类型创建一个新名称。类型别名使用 type 关键字定义。

  • 类型别名示例

    type Point = { x: number; y: number };
    type Coordinate = Point & { z: number };
    
    const coord: Coordinate = { x: 1, y: 2, z: 3 };
    

    类型别名可以用于创建简洁的类型表达式,也可以与交叉类型和联合类型结合使用。

4. 类型守卫(Type Guards)

类型守卫是运行时检查类型的机制,帮助 TypeScript 确定变量的具体类型。常见的类型守卫包括 typeofinstanceof 和自定义类型守卫。

  • typeof 类型守卫

    function getLength(value: string | string[]): number {
      if (typeof value === "string") {
        return value.length;
      } else {
        return value.length;
      }
    }
    
  • instanceof 类型守卫

    class Dog {
      bark() {}
    }
    
    class Cat {
      meow() {}
    }
    
    function speak(animal: Dog | Cat) {
      if (animal instanceof Dog) {
        animal.bark();
      } else {
        animal.meow();
      }
    }
    
  • 自定义类型守卫

    function isDog(animal: Dog | Cat): animal is Dog {
      return (animal as Dog).bark !== undefined;
    }
    
    function speak(animal: Dog | Cat) {
      if (isDog(animal)) {
        animal.bark();
      } else {
        animal.meow();
      }
    }
    
5. 映射类型(Mapped Types)

映射类型允许基于现有类型创建新的类型。使用 keyof 操作符和索引签名可以实现映射类型。

  • 基本映射类型
    type ReadOnly<T> = {
      readonly [P in keyof T]: T[P];
    };
    
    interface Person {
      name: string;
      age: number;
    }
    
    type ReadOnlyPerson = ReadOnly<Person>;
    
    const person: ReadOnlyPerson = { name: "Alice", age: 30 };
    // person.name = "Bob"; // 错误:不能修改 readonly 属性
    
6. 条件类型(Conditional Types)

条件类型允许根据某些条件选择不同的类型。条件类型使用 T extends U ? X : Y 语法。

  • 条件类型示例
    type TrueType = true extends true ? "Yes" : "No"; // "Yes"
    type FalseType = false extends true ? "Yes" : "No"; // "No"
    
    type IsString<T> = T extends string ? "String" : "Not String";
    type Test1 = IsString<string>; // "String"
    type Test2 = IsString<number>; // "Not String"
    
7. 索引类型(Index Types)

索引类型用于根据类型的键获取对应的值类型。使用 keyof[P in keyof T] 来操作索引类型。

  • 基本索引类型

    interface Person {
      name: string;
      age: number;
    }
    
    type PersonKeys = keyof Person; // "name" | "age"
    type PersonValues = Person[PersonKeys]; // string | number
    

    索引类型非常适合从对象类型中提取属性和处理动态键值。

8. 字面量类型(Literal Types)

字面量类型是对具体值的类型定义,使得变量的值可以限制为特定的字面量值。

  • 字面量类型示例
    type Direction = "left" | "right" | "up" | "down";
    
    function move(direction: Direction) {
      console.log(`Moving ${direction}`);
    }
    
    move("left"); // 合法
    move("right"); // 合法
    move("forward"); // 错误:类型“"forward"”不在“Direction”中
    

十、Symbol

Symbol 是 ES6 引入的一种新的基本数据类型,用于生成唯一的标识符。每个 Symbol 值都是唯一的,因此可以用于对象属性的键,避免了属性名冲突的问题。TypeScript 对 Symbol 类型进行了支持,可以在类型系统中使用它。

1. 创建 Symbol

使用 Symbol 函数可以创建一个新的 Symbol 实例。每次调用 Symbol 函数都会返回一个唯一的 Symbol 值。

  • 创建 Symbol

    const sym1 = Symbol('description');
    const sym2 = Symbol('description');
    
    console.log(sym1 === sym2); // false,两个 Symbol 是不同的
    

    在上面的例子中,sym1sym2 是不同的 Symbol 值,即使它们具有相同的描述。

2. 使用 Symbol 作为对象属性

Symbol 类型的值通常用于对象的属性键,避免属性名冲突和保护对象的内部属性。

  • 使用 Symbol 作为对象属性

    const sym = Symbol('mySymbol');
    
    const obj = {
      [sym]: 'value'
    };
    
    console.log(obj[sym]); // 输出 "value"
    

    在这个例子中,sym 是一个唯一的 Symbol,用于定义对象 obj 的一个属性。使用 Symbol 作为属性键可以避免属性名冲突。

3. Symbol 类型的声明

TypeScript 中的 Symbol 类型与 ES6 标准一致,可以作为对象的属性类型。

  • 声明 Symbol 类型
    const sym1: symbol = Symbol('foo');
    const sym2: symbol = Symbol('bar');
    
    const obj: { [key: symbol]: string } = {
      [sym1]: 'value1',
      [sym2]: 'value2'
    };
    
    console.log(obj[sym1]); // 输出 "value1"
    console.log(obj[sym2]); // 输出 "value2"
    
4. 使用 Symbol 进行枚举

Symbol 可以用作枚举的值,这样可以创建一组唯一的标识符。

  • 使用 Symbol 进行枚举
    enum Status {
      Pending = Symbol('pending'),
      InProgress = Symbol('inProgress'),
      Done = Symbol('done')
    }
    
    function handleStatus(status: Status) {
      switch (status) {
        case Status.Pending:
          console.log('Pending');
          break;
        case Status.InProgress:
          console.log('In Progress');
          break;
        case Status.Done:
          console.log('Done');
          break;
        default:
          console.log('Unknown status');
      }
    }
    
    handleStatus(Status.Pending); // 输出 "Pending"
    
5. SymbolSymbol.for / Symbol.keyFor

Symbol.forSymbol.keyFor 是 ES6 引入的全局 Symbol 注册和查找机制。通过 Symbol.for,可以创建一个全局唯一的 Symbol,并根据描述查找现有的 Symbol

  • 使用 Symbol.for

    const globalSym1 = Symbol.for('globalSymbol');
    const globalSym2 = Symbol.for('globalSymbol');
    
    console.log(globalSym1 === globalSym2); // true,两个 Symbol 是相同的
    

    Symbol.for 会返回全局注册的 Symbol,即使多次调用相同的描述也会得到相同的 Symbol

  • 使用 Symbol.keyFor

    const globalSym = Symbol.for('globalSymbol');
    const description = Symbol.keyFor(globalSym);
    
    console.log(description); // 输出 "globalSymbol"
    

    Symbol.keyFor 可以用于获取全局 Symbol 的描述字符串。

6. Symbol 的类型保护

在 TypeScript 中,可以使用类型保护来确保处理的是 Symbol 类型。

  • 类型保护示例

    function isSymbol(value: any): value is Symbol {
      return typeof value === 'symbol';
    }
    
    const sym = Symbol('test');
    
    if (isSymbol(sym)) {
      console.log('sym is a Symbol');
    }
    

    isSymbol 函数用于检查一个值是否是 Symbol 类型。

十一、迭代器和生成器

1. 迭代器(Iterators)

迭代器是一种对象,它定义了如何访问一个集合中的每个元素。一个迭代器对象必须实现一个 next 方法,该方法返回一个对象,该对象包含 valuedone 属性。

  • 迭代器的基本示例

    class Counter {
      private count: number = 0;
    
      next(): { value: number; done: boolean } {
        this.count++;
        return { value: this.count, done: false };
      }
    }
    
    const counter = new Counter();
    console.log(counter.next()); // { value: 1, done: false }
    console.log(counter.next()); // { value: 2, done: false }
    

    在这个例子中,Counter 类实现了一个简单的迭代器,next 方法返回一个包含当前计数值的对象。

  • 标准迭代器接口

    interface IteratorResult<T> {
      value: T;
      done: boolean;
    }
    
    interface Iterator<T> {
      next(value?: any): IteratorResult<T>;
    }
    

    TypeScript 中的标准迭代器接口包括 IteratorIteratorResult,用于定义迭代器的行为和结果。

2. 可迭代对象(Iterable Objects)

可迭代对象是实现了 Symbol.iterator 方法的对象。Symbol.iterator 方法返回一个迭代器对象。标准的内置可迭代对象包括数组、字符串、集合和映射等。

  • 实现可迭代对象

    class MyIterable implements Iterable<number> {
      private data: number[] = [1, 2, 3];
    
      [Symbol.iterator](): Iterator<number> {
        let index = 0;
        return {
          next: () => {
            if (index < this.data.length) {
              return { value: this.data[index++], done: false };
            } else {
              return { value: undefined, done: true };
            }
          }
        };
      }
    }
    
    const iterable = new MyIterable();
    for (const value of iterable) {
      console.log(value); // 输出 1 2 3
    }
    

    在这个例子中,MyIterable 类实现了 Iterable 接口,并定义了 Symbol.iterator 方法,返回一个迭代器对象。

3. 生成器(Generators)

生成器是特殊的迭代器,允许在函数中使用 yield 关键字来逐步生成序列。生成器函数返回一个生成器对象,该对象实现了 Iterator 接口。

  • 生成器函数示例

    function* countUpTo(max: number): Generator<number> {
      let count = 1;
      while (count <= max) {
        yield count++;
      }
    }
    
    const counter = countUpTo(3);
    console.log(counter.next()); // { value: 1, done: false }
    console.log(counter.next()); // { value: 2, done: false }
    console.log(counter.next()); // { value: 3, done: false }
    console.log(counter.next()); // { value: undefined, done: true }
    

    生成器函数使用 function* 语法定义,yield 关键字用于生成值。每次调用 next 方法时,生成器会从上一次 yield 停止的地方继续执行。

4. 生成器的高级用法

生成器还支持 returnthrow 方法,用于控制生成器的退出和异常处理。

  • 生成器的 returnthrow 方法

    function* generatorWithReturn() {
      yield 1;
      yield 2;
      return 'done';
    }
    
    const gen = generatorWithReturn();
    console.log(gen.next()); // { value: 1, done: false }
    console.log(gen.next()); // { value: 2, done: false }
    console.log(gen.next()); // { value: 'done', done: true }
    

    生成器可以通过 return 返回一个最终值,该值将成为最后一次 next 调用的返回值。

  • 生成器的异常处理

    function* generatorWithError() {
      try {
        yield 1;
        yield 2;
      } catch (e) {
        console.log('Caught an error:', e);
      }
    }
    
    const gen = generatorWithError();
    console.log(gen.next()); // { value: 1, done: false }
    console.log(gen.throw(new Error('Something went wrong'))); // 捕获错误并输出
    

    使用 throw 方法可以在生成器中抛出错误并进行处理。

5. 使用生成器创建迭代器

生成器可以用于创建复杂的迭代器,处理各种序列和流。

  • 生成器创建迭代器示例

    function* range(start: number, end: number): Generator<number> {
      for (let i = start; i < end; i++) {
        yield i;
      }
    }
    
    const numbers = range(1, 5);
    for (const num of numbers) {
      console.log(num); // 输出 1 2 3 4
    }
    

    生成器函数 range 创建了一个迭代器,用于生成从 startend 之间的数字序列。

十二、模块

模块是 JavaScript 和 TypeScript 中用于组织代码和管理依赖的机制。模块使得代码更具可维护性和可重用性。TypeScript 的模块系统基于 ECMAScript 2015(ES6)的模块规范,但也提供了额外的功能和支持。

1. 导出(Export)

在 TypeScript 中,可以使用 export 关键字将变量、函数、类、接口等从一个模块导出,以便在其他模块中使用。

  • 命名导出

    // utils.ts
    export const PI = 3.14;
    
    export function add(a: number, b: number): number {
      return a + b;
    }
    
    export class MathUtils {
      static square(x: number): number {
        return x * x;
      }
    }
    

    在上面的代码中,PIaddMathUtils 都是通过命名导出导出的。

  • 默认导出

    // math.ts
    export default class MathService {
      multiply(a: number, b: number): number {
        return a * b;
      }
    }
    

    在这个示例中,MathService 是默认导出的类。每个模块只能有一个默认导出。

2. 导入(Import)

可以使用 import 关键字在其他模块中引入已导出的成员。

  • 导入命名导出

    // main.ts
    import { PI, add, MathUtils } from './utils';
    
    console.log(PI); // 3.14
    console.log(add(2, 3)); // 5
    console.log(MathUtils.square(4)); // 16
    

    在这个例子中,PIaddMathUtilsutils 模块中导入并使用。

  • 导入默认导出

    // app.ts
    import MathService from './math';
    
    const math = new MathService();
    console.log(math.multiply(2, 3)); // 6
    

    默认导出的 MathService 可以通过自定义名称导入。

  • 导入所有成员

    // app.ts
    import * as Utils from './utils';
    
    console.log(Utils.PI); // 3.14
    console.log(Utils.add(2, 3)); // 5
    console.log(Utils.MathUtils.square(4)); // 16
    

    使用 import * as 语法可以将模块的所有导出成员导入到一个对象中。

3. 模块解析

TypeScript 的模块解析策略定义了如何查找模块文件。默认的解析策略包括基于文件系统的解析,类似于 Node.js 的解析规则。

  • 模块解析示例

    // tsconfig.json
    {
      "compilerOptions": {
        "module": "commonjs",
        "baseUrl": "./",
        "paths": {
          "*": ["src/*"]
        }
      }
    }
    

    tsconfig.json 中,baseUrlpaths 可以用于配置模块解析的基路径和路径映射。

4. 模块的声明

如果需要在 TypeScript 中使用没有类型定义的 JavaScript 库,可以使用模块声明来定义模块的类型。

  • 模块声明示例

    // custom-lib.d.ts
    declare module 'custom-lib' {
      export function doSomething(): void;
    }
    

    在这个示例中,custom-lib.d.ts 文件声明了一个模块 custom-lib,并定义了它的导出成员。

5. 动态导入(Dynamic Imports)

动态导入允许在运行时按需加载模块。这对于实现代码分割和懒加载非常有用。

  • 动态导入示例

    async function loadModule() {
      const module = await import('./some-module');
      module.someFunction();
    }
    

    使用 import() 语法可以动态地加载模块,返回一个 Promise 对象。

6. ES 模块与 CommonJS 模块

TypeScript 支持多种模块系统,包括 ES 模块和 CommonJS 模块。可以在 tsconfig.json 中指定模块系统。

  • ES 模块

    // tsconfig.json
    {
      "compilerOptions": {
        "module": "es6"
      }
    }
    
  • CommonJS 模块

    // tsconfig.json
    {
      "compilerOptions": {
        "module": "commonjs"
      }
    }
    

    根据项目需求选择适当的模块系统。

7. 模块的命名空间(Namespace)

在 TypeScript 中,模块和命名空间可以用来组织代码。模块用于导出和导入成员,而命名空间用于在同一文件或不同文件中组织代码。

  • 命名空间示例

    namespace Utilities {
      export function log(message: string): void {
        console.log(message);
      }
    }
    
    Utilities.log("Hello, world!"); // 输出 "Hello, world!"
    

    命名空间的使用方式类似于模块,但它们更适合在同一文件中组织代码。

十三、命名空间

命名空间是 TypeScript 中一种用于组织代码的机制,允许在同一文件或不同文件中将相关的代码进行分组。命名空间主要用于将相关的函数、变量、类和接口等放在一个逻辑组中,从而避免全局命名冲突。

1. 创建命名空间

命名空间使用 namespace 关键字来定义。所有在命名空间内的成员都被封装在这个命名空间下。

  • 基本示例

    namespace Geometry {
      export interface Point {
        x: number;
        y: number;
      }
    
      export class Circle {
        constructor(public radius: number) {}
      }
    
      export function distance(p1: Point, p2: Point): number {
        return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
      }
    }
    
    const p1: Geometry.Point = { x: 0, y: 0 };
    const p2: Geometry.Point = { x: 3, y: 4 };
    
    const circle = new Geometry.Circle(10);
    console.log(Geometry.distance(p1, p2)); // 输出 5
    

    在上面的例子中,Geometry 是一个命名空间,其中包含了一个接口 Point、一个类 Circle 和一个函数 distance。这些成员通过 export 关键字公开,允许在命名空间外部访问。

2. 嵌套命名空间

命名空间可以嵌套定义,形成多层结构,这样可以更细致地组织代码。

  • 嵌套命名空间示例

    namespace Utilities {
      export namespace Math {
        export function add(a: number, b: number): number {
          return a + b;
        }
    
        export function subtract(a: number, b: number): number {
          return a - b;
        }
      }
    
      export namespace String {
        export function toUpperCase(s: string): string {
          return s.toUpperCase();
        }
      }
    }
    
    console.log(Utilities.Math.add(2, 3)); // 输出 5
    console.log(Utilities.String.toUpperCase('hello')); // 输出 HELLO
    

    在这个示例中,Utilities 命名空间包含了两个嵌套的命名空间 MathString,分别处理数学操作和字符串操作。

3. 内部模块(旧版命名空间)

在 TypeScript 1.5 之前,命名空间被称为内部模块。旧版命名空间在 namespace 关键字出现之前使用 module 关键字定义。现代 TypeScript 推荐使用 namespace 关键字来代替 module 关键字。

  • 内部模块(旧版)示例

    module Geometry {
      export interface Point {
        x: number;
        y: number;
      }
    
      export class Circle {
        constructor(public radius: number) {}
      }
    }
    

    尽管 module 关键字仍然被支持,但现代 TypeScript 推荐使用 namespace 关键字。

4. 解析命名空间

TypeScript 在编译时会将命名空间和模块转换为 JavaScript 代码。命名空间中的代码通常会被编译到同一个文件中,因此需要适当配置 TypeScript 编译器选项来处理命名空间。

  • tsconfig.json 配置示例

    {
      "compilerOptions": {
        "module": "commonjs",
        "target": "es6"
      }
    }
    

    tsconfig.json 中,设置 "module" 选项为 "commonjs" 可以将命名空间编译为 CommonJS 模块。

5. 使用命名空间进行模块化

虽然现代 JavaScript 模块系统(如 ES6 模块和 CommonJS 模块)已经取代了许多命名空间的使用,但命名空间仍然有其独特的用途,特别是在大型项目中对内部模块的组织和管理。

  • 命名空间与模块结合示例

    // shapes.ts
    namespace Shapes {
      export interface Shape {
        area(): number;
      }
    
      export class Square implements Shape {
        constructor(private side: number) {}
        area() {
          return this.side * this.side;
        }
      }
    }
    
    export = Shapes;
    
    // app.ts
    import Shapes = require('./shapes');
    
    const square = new Shapes.Square(5);
    console.log(square.area()); // 输出 25
    

    在这个示例中,shapes.ts 文件使用命名空间组织代码,并将其导出为模块。app.ts 文件通过 import = require 语法导入命名空间。

6. 命名空间与全局声明

命名空间可以与全局声明结合使用,在全局作用域中声明类型或功能。

  • 全局命名空间示例

    // global.d.ts
    namespace GlobalNamespace {
      export function globalFunction(): void;
    }
    
    // global.ts
    namespace GlobalNamespace {
      export function globalFunction() {
        console.log('Global function called');
      }
    }
    

    在这个示例中,global.d.ts 文件定义了一个全局命名空间,而 global.ts 文件实现了该命名空间中的函数。

十四、声明合并

声明合并是 TypeScript 中的一个特性,允许将多个声明合并成一个声明。这使得可以在不同的地方扩展和修改已有的类型、接口、命名空间等。声明合并在处理第三方库、扩展现有类型或者在大型项目中组织代码时非常有用。

1. 接口声明合并

接口(interface)声明合并允许在多个地方声明同一个接口,TypeScript 会将这些声明合并成一个接口。

  • 接口合并示例

    interface User {
      name: string;
      age: number;
    }
    
    // 在其他地方扩展接口
    interface User {
      email: string;
    }
    
    const user: User = {
      name: "Alice",
      age: 30,
      email: "alice@example.com"
    };
    

    在这个例子中,User 接口的两个声明被合并成一个接口,包含 nameageemail 属性。

2. 命名空间声明合并

命名空间(namespace)声明合并允许将多个命名空间声明合并成一个命名空间。这样可以在不同的文件中组织相关的功能。

  • 命名空间合并示例

    namespace Utilities {
      export function log(message: string): void {
        console.log(message);
      }
    }
    
    namespace Utilities {
      export function warn(message: string): void {
        console.warn(message);
      }
    }
    
    Utilities.log("Info message");
    Utilities.warn("Warning message");
    

    在这个例子中,Utilities 命名空间的两个声明被合并,包含 logwarn 方法。

3. 类的合并(静态成员)

类的静态成员可以通过声明合并进行扩展,这对于为类添加额外的静态方法或属性非常有用。

  • 类的静态成员合并示例

    class MyClass {
      static greeting: string = "Hello";
    }
    
    // 在其他地方扩展静态成员
    namespace MyClass {
      export function sayHello() {
        console.log(MyClass.greeting);
      }
    }
    
    MyClass.sayHello(); // 输出 "Hello"
    

    在这个示例中,MyClass 的静态成员通过 namespace 扩展,添加了一个静态方法 sayHello

4. 函数重载

函数重载允许为同一个函数定义多个不同的签名,从而使函数能够处理不同类型的输入。

  • 函数重载示例

    function greet(person: string): string;
    function greet(person: string, age: number): string;
    function greet(person: string, age?: number): string {
      if (age !== undefined) {
        return `Hello ${person}, you are ${age} years old`;
      } else {
        return `Hello ${person}`;
      }
    }
    
    console.log(greet("Alice")); // 输出 "Hello Alice"
    console.log(greet("Bob", 30)); // 输出 "Hello Bob, you are 30 years old"
    

    在这个例子中,greet 函数有两个不同的签名,分别处理只有名字和名字加年龄的情况。

5. 模块的合并(外部模块和命名空间)

模块系统(ES6 模块和 CommonJS 模块)中不能直接进行声明合并,但可以通过命名空间和模块结合来实现类似的效果。

  • 模块和命名空间结合示例

    // shapes.ts
    namespace Shapes {
      export interface Point {
        x: number;
        y: number;
      }
    }
    
    export = Shapes;
    
    // app.ts
    import Shapes = require('./shapes');
    
    namespace Shapes {
      export function distance(p1: Shapes.Point, p2: Shapes.Point): number {
        return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
      }
    }
    
    const p1: Shapes.Point = { x: 0, y: 0 };
    const p2: Shapes.Point = { x: 3, y: 4 };
    
    console.log(Shapes.distance(p1, p2)); // 输出 5
    

    在这个示例中,Shapes 命名空间通过模块和命名空间结合进行扩展,增加了一个 distance 函数。

6. 声明合并的注意事项
  • 命名冲突:当多个声明合并时,确保没有命名冲突或不一致的情况。TypeScript 会自动合并声明,但需要确保合并后的结果符合预期。
  • 可维护性:过度使用声明合并可能导致代码难以维护。尽量在组织代码时保持简单和清晰。
  • 模块系统:在模块系统中,尽量使用 ES6 模块的导出和导入机制,而不是依赖声明合并来扩展模块功能。

装饰器(Decorators)是 TypeScript 提供的一种特殊类型的声明,用于修改类、方法、属性或参数的行为。装饰器的使用在 Angular 和其他 TypeScript 框架中非常常见,它们为代码提供了强大的元编程能力。

十五、装饰器

1. 装饰器简介

装饰器是一种特殊的 TypeScript 语法,用于在运行时附加元数据或修改类的行为。装饰器可以用于以下几个方面:

  • 类装饰器:用于修饰类。
  • 方法装饰器:用于修饰类的方法。
  • 属性装饰器:用于修饰类的属性。
  • 参数装饰器:用于修饰类的方法参数。

装饰器需要在 tsconfig.json 文件中启用 experimentalDecorators 选项。

  • 启用装饰器
    {
      "compilerOptions": {
        "experimentalDecorators": true
      }
    }
    
2. 类装饰器

类装饰器用于修饰类的构造函数,可以用于修改类的定义或添加额外的功能。

  • 类装饰器示例

    function logClass(target: Function) {
      console.log(`Class: ${target.name}`);
    }
    
    @logClass
    class MyClass {
      constructor(public name: string) {}
    }
    
    // 输出:Class: MyClass
    const myClass = new MyClass('Alice');
    

    在这个示例中,logClass 装饰器用于修饰 MyClass 类,装饰器函数会在类被定义时执行。

3. 方法装饰器

方法装饰器用于修饰类的方法,可以用于修改方法的行为或添加日志等功能。

  • 方法装饰器示例

    function logMethod(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
      const originalMethod = descriptor.value;
    
      descriptor.value = function (...args: any[]) {
        console.log(`Method: ${String(propertyKey)}, Arguments: ${args}`);
        return originalMethod.apply(this, args);
      };
    }
    
    class MyClass {
      @logMethod
      greet(name: string) {
        return `Hello, ${name}`;
      }
    }
    
    const myClass = new MyClass();
    myClass.greet('Alice'); // 输出:Method: greet, Arguments: Alice
    

    在这个示例中,logMethod 装饰器用于修饰 greet 方法,装饰器函数会在方法调用时执行,输出方法名称和参数。

4. 属性装饰器

属性装饰器用于修饰类的属性,可以用于修改属性的行为或添加元数据。

  • 属性装饰器示例

    function formatProperty(target: any, propertyKey: string | symbol) {
      let value = target[propertyKey];
    
      const getter = () => {
        return `Formatted: ${value}`;
      };
    
      const setter = (newValue: any) => {
        value = newValue;
      };
    
      Object.defineProperty(target, propertyKey, {
        get: getter,
        set: setter
      });
    }
    
    class MyClass {
      @formatProperty
      name: string = 'Default Name';
    }
    
    const myClass = new MyClass();
    console.log(myClass.name); // 输出:Formatted: Default Name
    

    在这个示例中,formatProperty 装饰器用于修饰 name 属性,装饰器函数修改了属性的 getter 和 setter。

5. 参数装饰器

参数装饰器用于修饰方法的参数,可以用于添加元数据或修改参数的行为。

  • 参数装饰器示例

    function logParameter(target: any, propertyKey: string | symbol, parameterIndex: number) {
      const existingParameters: number[] = Reflect.getOwnMetadata('logParameters', target, propertyKey) || [];
      existingParameters.push(parameterIndex);
      Reflect.defineMetadata('logParameters', existingParameters, target, propertyKey);
    }
    
    class MyClass {
      greet(@logParameter name: string) {
        console.log(`Hello, ${name}`);
      }
    }
    
    const myClass = new MyClass();
    myClass.greet('Alice'); // 输出:Hello, Alice
    

    在这个示例中,logParameter 装饰器用于修饰 greet 方法的参数,记录参数的索引。

6. 装饰器工厂

装饰器工厂是返回装饰器的函数,允许传递参数给装饰器。

  • 装饰器工厂示例

    function log(level: string) {
      return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
    
        descriptor.value = function (...args: any[]) {
          console.log(`[${level}] Method: ${String(propertyKey)}, Arguments: ${args}`);
          return originalMethod.apply(this, args);
        };
      };
    }
    
    class MyClass {
      @log('INFO')
      greet(name: string) {
        return `Hello, ${name}`;
      }
    }
    
    const myClass = new MyClass();
    myClass.greet('Alice'); // 输出:[INFO] Method: greet, Arguments: Alice
    

    在这个示例中,log 装饰器工厂接受一个 level 参数,并返回一个装饰器。

7. 装饰器的实际应用

装饰器在实际应用中非常有用,尤其是在以下场景:

  • 框架和库:如 Angular 和 NestJS,广泛使用装饰器来定义和配置组件、服务、路由等。
  • 代码生成:在编译时生成额外的代码或元数据。
  • 日志记录和性能监控:为方法添加日志记录或性能监控功能。

Mixins 是一种设计模式,允许在类中复用方法和属性。它们是一种灵活的方式来构建和组合类功能,避免了传统的继承模型的限制。TypeScript 支持 mixins,通过将多个类的行为组合到一个类中来实现功能复用。

十六、Mixins

1. Mixins 概述

Mixins 是一种模式,用于将多个类的行为组合到一个类中,从而实现功能复用。它避免了单继承模型的限制,使得一个类能够拥有来自多个来源的功能。

  • Mixins 的示例

    class A {
      a() {
        console.log('A');
      }
    }
    
    class B {
      b() {
        console.log('B');
      }
    }
    
    class C {}
    
    // 通过混入 A 和 B 的功能到 C 中
    function applyMixins(derivedCtor: any, baseCtors: any[]) {
      baseCtors.forEach(baseCtor => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
          derivedCtor.prototype[name] = baseCtor.prototype[name];
        });
      });
    }
    
    applyMixins(C, [A, B]);
    
    const c = new C();
    c.a(); // 输出: 'A'
    c.b(); // 输出: 'B'
    

    在这个示例中,applyMixins 函数将 AB 的方法混入到 C 中,使得 C 实例可以调用 ab 方法。

2. 实现 Mixins

要实现 mixins,需要将多个类的行为合并到一个目标类中。通常,这涉及到将一个类的原型方法复制到另一个类的原型上。

  • 实现 Mixins 的步骤

    1. 定义基础类: 定义需要混入功能的基础类。

      class A {
        a() {
          console.log('A');
        }
      }
      
      class B {
        b() {
          console.log('B');
        }
      }
      
    2. 定义目标类: 定义目标类,并使用 mixins 函数将基础类的功能混入目标类。

      class C {}
      
      function applyMixins(derivedCtor: any, baseCtors: any[]) {
        baseCtors.forEach(baseCtor => {
          Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
            derivedCtor.prototype[name] = baseCtor.prototype[name];
          });
        });
      }
      
      applyMixins(C, [A, B]);
      
    3. 使用目标类: 创建目标类的实例,并调用混入的方法。

      const c = new C();
      c.a(); // 输出: 'A'
      c.b(); // 输出: 'B'
      
3. TypeScript 类型安全的 Mixins

为了确保 mixins 在 TypeScript 中的类型安全,可以使用泛型和类型约束来更精确地定义 mixins 的行为。

  • 类型安全的 Mixins 示例

    type Constructor<T = {}> = new (...args: any[]) => T;
    
    function MixinA<TBase extends Constructor>(Base: TBase) {
      return class extends Base {
        a() {
          console.log('A');
        }
      };
    }
    
    function MixinB<TBase extends Constructor>(Base: TBase) {
      return class extends Base {
        b() {
          console.log('B');
        }
      };
    }
    
    class Base {}
    
    class Combined extends MixinA(MixinB(Base)) {}
    
    const instance = new Combined();
    instance.a(); // 输出: 'A'
    instance.b(); // 输出: 'B'
    

    在这个示例中,MixinAMixinB 是类型安全的 mixins,它们分别添加方法 abCombined 类使用了这些 mixins,实现了功能的组合。

4. 使用 Mixins 进行复杂功能组合

Mixins 可以用来实现更复杂的功能组合,例如组合多个行为和特性。

  • 复杂功能组合示例

    class Logger {
      log(message: string) {
        console.log(`[LOG] ${message}`);
      }
    }
    
    class Authenticator {
      authenticate(user: string) {
        console.log(`Authenticating ${user}`);
      }
    }
    
    class Application {}
    
    applyMixins(Application, [Logger, Authenticator]);
    
    const app = new Application();
    app.log('Starting application...'); // 输出: [LOG] Starting application...
    app.authenticate('Alice'); // 输出: Authenticating Alice
    

    在这个示例中,LoggerAuthenticator 的功能被混入到 Application 类中,实现了日志记录和身份验证功能。

5. 使用 Mixins 的注意事项
  • 命名冲突:当混入多个类时,确保方法和属性没有命名冲突。混入的类中如果有同名的方法,后混入的类的方法会覆盖之前的方法。
  • 性能考虑:过度使用 mixins 可能会导致类变得复杂,影响代码的可读性和维护性。使用 mixins 时应保持代码的清晰和可管理性。
  • 类型安全:在 TypeScript 中使用 mixins 时,确保正确设置类型,以避免类型错误。