跳至主要內容

樱桃茶大约 23 分钟

instanceof类型保护是 TypeScript 中的一种机制,用来确保变量是某个特定类或构造函数的实例。这种类型保护可以让 TypeScript 编译器理解在特定的作用域内变量属于哪种类型,从而能够提供正确的属性和方法提示,并确保类型安全。

使用instanceof进行类型保护的基本格式如下:

if (value instanceof SomeClass) {
  // 在这个块中,TypeScript知道'value'是'SomeClass'的实例。
  // 这里可以安全地访问'SomeClass'中定义的属性和方法。
}
  • instanceof检查左边的对象是否是右边类(或构造函数)的实例。
  • 运行时会检查右边的原型链是否存在于左边对象的原型链上。

举例说明instanceof类型保护:

class Bird {
  fly() {
    console.log("The bird is flying.");
  }
}

class Fish {
  swim() {
    console.log("The fish is swimming.");
  }
}

function move(animal: Bird | Fish) {
  if (animal instanceof Bird) {
    animal.fly();
  } else if (animal instanceof Fish) {
    animal.swim();
  }
}

const myBird = new Bird();
move(myBird); // 输出:The bird is flying.

const myFish = new Fish();
move(myFish); // 输出:The fish is swimming.

在上面的例子中,move函数接受一个参数animal,它可以是Bird类的实例,也可以是Fish类的实例。我们使用instanceof来判断animal的具体类型:

  • 如果animalBird的实例,调用fly方法。
  • 如果animalFish的实例,调用swim方法。

这样,即使在函数外部不清楚传入的animal具体类型,通过instanceof我们依然能在函数内部确定类型,并执行安全的操作。

  • TypeScript 中的类是一种编程结构,用于创建含有属性和方法的对象。它是面向对象编程的核心概念之一。

  • 类的基本语法与 JavaScript ES6 非常相似,但 TypeScript 提供了更多特性,比如类型注解和修饰符。

  • 定义类:

    class Person {
      name: string;
    
      constructor(name: string) {
        this.name = name;
      }
    
      greet() {
        return "Hello, " + this.name;
      }
    }
    
    let user = new Person("Alice");
    console.log(user.greet()); // 输出: Hello, Alice
    
  • 在这个例子中,我们定义了一个Person类,它拥有一个名为name的属性和一个greet方法。

  • 继承:

    • TypeScript 支持基于类的继承,允许你创建一个类(子类)作为另一个类(父类)的派生。
    class Employee extends Person {
      employeeID: number;
    
      constructor(name: string, employeeID: number) {
        super(name); // 调用父类的constructor
        this.employeeID = employeeID;
      }
    
      work() {
        return `${this.name} is working.`;
      }
    }
    
    let emp = new Employee("Bob", 123);
    console.log(emp.work()); // 输出: Bob is working.
    
  • 在这个例子中,Employee类继承自Person类。使用extends关键字来实现继承。Employee类添加了新的属性和方法。

  • 访问修饰符:

    • TypeScript 支持三种访问修饰符:public(默认),privateprotected
    class Person {
      private name: string; // 不能在类外部访问
    
      constructor(name: string) {
        this.name = name;
      }
    
      public greet() {
        return `Hello, ${this.name}`;
      }
    }
    
    let user = new Person("Alice");
    console.log(user.greet()); // 正确
    //console.log(user.name); // 错误,因为name是私有的
    
  • 在这个例子中,name属性被标记为private,意味着它无法在Person类的外部直接访问。而greet方法是public的,可以在任何地方被调用。

  • 抽象类(abstract classes):

    • 抽象类是供其他类继承的基类,不能被实例化。
    abstract class Animal {
      abstract makeSound(): void; // 必须在派生类中实现
    
      move(): void {
        console.log("Moving along!");
      }
    }
    
    class Dog extends Animal {
      makeSound() {
        console.log("Woof! Woof!");
      }
    }
    
    let pet = new Dog();
    pet.makeSound(); // 输出: Woof! Woof!
    pet.move(); // 输出: Moving along!
    
  • 抽象类Animal定义了一个抽象方法makeSound,该方法在派生类Dog中被具体实现。

  • 实现接口(implements interfaces):

    • 类可以实现接口来保证遵守特定的契约。
    interface IWorker {
      work(): void;
    }
    
    class Worker implements IWorker {
      work() {
        console.log("Working...");
      }
    }
    
    let worker = new Worker();
    worker.work(); // 输出: Working...
    
  • 在这个例子中,Worker类通过implements关键字实现了IWorker接口,确保Worker类具有接口定义的work方法。

继承

继承是面向对象编程的一个基本概念,它允许你创建一个类(称为子类)来继承另一个类(称为父类或基类)的属性和方法。在 TypeScript 中,继承的目的是为了代码复用和建立一个逻辑上的层级关系。

以下是有关 TypeScript 中继承的一些要点:

  • 使用extends关键字来实现继承。子类通过extends关键字连接到父类。
  • 子类可以调用父类的方法和访问父类的属性,除非这些成员是私有的(使用private关键字声明)。
  • 如果子类有自己的构造函数(constructor),它必须调用super(),并且这个调用必须是第一个语句。super是父类的构造函数。
  • 子类可以覆盖(override)父类的方法以提供更具体的实现。
  • 你可以使用protected关键字来确保只有该类及其子类可以访问特定的成员。

例子:

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); // 必须调用super(),它将执行父类的构造函数
  }

  move(distanceInMeters: number = 5) {
    console.log("Slithering...");
    super.move(distanceInMeters); // 调用父类的move方法
  }
}

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

  move(distanceInMeters: number = 45) {
    console.log("Galloping...");
    super.move(distanceInMeters); // 调用父类的move方法
  }
}

let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");

sam.move(); // 输出: Slithering... Sammy the Python moved 5m.
tom.move(34); // 输出: Galloping... Tommy the Palomino moved 34m.

在这个例子中,SnakeHorse都是Animal的子类,它们继承了Animal的属性和方法,并且提供了自己对move方法的特定实现。注意在SnakeHorse中如何使用super.move()来调用父类的move方法。

公共,私有与受保护的修饰符

在 TypeScript 中,类的成员(包括属性和方法)可以通过使用修饰符来控制其可访问性。这些修饰符主要有三种:public(公共的)、private(私有的)和protected(受保护的)。下面我们将逐一解释,并通过例子展示它们的用法。

  • public

    • 这是默认的修饰符,意味着成员是从任何地方都可以访问的。

    • 示例:

      class Animal {
        public name: string;
        constructor(theName: string) {
          this.name = theName;
        }
      }
      
      let cat = new Animal("Whiskers");
      console.log(cat.name); // 可以从外部访问name
      
  • private

    • 当成员被标记为private时,它就不能在声明它的类的外部访问。

    • TypeScript 使用的是结构性类型系统,当比较两种不同的类型时,并不关心它们来自哪里,只关心它们的结构是否兼容。但对于privateprotected成员,如果其中一个类型包含了一个private成员,那么只有当另一个类型也含有这个private成员,并且它们都是同一声明时,这两个类型才被认为是兼容的。

    • 示例:

      class Animal {
        private name: string;
        constructor(theName: string) {
          this.name = theName;
        }
      }
      
      let cat = new Animal("Whiskers");
      // console.log(cat.name); // 错误: 'name' 是私有的.
      
  • protected

    • protected修饰符与private类似,差别在于protected成员在派生类中仍然可以访问。

    • 示例:

      class Animal {
        protected name: string;
        constructor(name: string) {
          this.name = name;
        }
      }
      
      class Cat extends Animal {
        constructor(name: string) {
          super(name);
        }
      
        public getName() {
          return this.name; // 正确: 'name' 在派生类中是可访问的
        }
      }
      
      let cat = new Cat("Whiskers");
      console.log(cat.getName()); // 正确
      // console.log(cat.name); // 错误: 'name' 是受保护的, 不能在外部访问
      

每种修饰符都有其特定的使用场景。public修饰符通常用于那些我们想要暴露给类的使用者的成员;private修饰符用于那些只在类内部使用、不希望外部访问的成员;而protected修饰符则是介于二者之间,它允许类的继承者访问这些成员,但仍然阻止外部的直接访问。这些修饰符帮助我们在大型项目中更好地进行封装和抽象,确保对象的状态和行为被正确管理。

默认为 public

在 TypeScript 中,类的成员(属性和方法)可以使用访问修饰符来控制其可访问性。有三种基本的访问修饰符:

  • public:成员是公开的,默认情况下所有的成员都是公开的,可以在任何地方被访问。
  • private:成员是私有的,只能在定义这些成员的类内部访问。
  • protected:成员是受保护的,它和私有成员类似,但在派生类中也可以访问。

以下是一些例子来说明公共、私有与受保护的修饰符:

class Animal {
  public name: string; // 明确标记为public,但即使不写public,name仍默认为public

  private secret: string; // 私有属性,只能在Animal类内部访问

  protected legCount: number; // 受保护属性,可以在Animal类及其子类中访问

  constructor(name: string, secret: string, legCount: number) {
    this.name = name;
    this.secret = secret;
    this.legCount = legCount;
  }

  public move(distanceInMeters: number): void {
    // 明确标记为public方法
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}

class Dog extends Animal {
  constructor(name: string, secret: string) {
    super(name, secret, 4); // 调用父类构造函数
  }

  public bark(): void {
    console.log("Woof! Woof!");
  }

  public showLegCount(): void {
    // 正常访问受保护成员
    console.log(`This dog has ${this.legCount} legs.`);
  }
}

const dog = new Dog("Rex", "I have a bone");
dog.move(10); // 输出: Rex moved 10m.
dog.bark(); // 输出: Woof! Woof!
dog.showLegCount(); // 输出: This dog has 4 legs.

// 下面的访问将会产生错误:
// console.log(dog.secret); // 错误: 属性“secret”为私有属性,只能在类“Animal”中访问
// console.log(dog.legCount); // 错误: 属性“legCount”受保护,只能在类“Animal”及其子类中访问

在上述代码中,name属性虽然标记为public,但即使我们没有显式地添加该标记,它也是公开可访问的。私有成员secret只能在Animal类内部访问,而受保护成员legCount既可以在Animal类内部访问,也可以在继承自AnimalDog类内部访问。尝试在类的外部访问这些非公开成员将导致编译错误。

理解 private

理解 private

  • TypeScript 中的类与 JavaScript 的 ES6 类非常相似, 但它提供了一些访问修饰符来控制成员(属性和方法)的可访问性。private 是这些修饰符之一。
  • 使用 private 修饰符声明的类成员,只能在其定义的类的内部访问。换句话说,如果你试图从类的实例或子类中访问私有成员,TypeScript 编译器会抛出一个错误。
  • 当你想要隐藏某个类成员不被外部访问,仅仅允许类自己的方法内部使用时,就应该使用 private

例子:

class Person {
  private name: string;

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

  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
}

let person = new Person("Alice");
person.greet(); // 正确: "Hello, my name is Alice"
console.log(person.name); // 错误: Property 'name' is private and only accessible within class 'Person'.
  • 在上面的例子中,属性 name 被标记为 private,因此它不能在 Person 类的实例外部被直接访问。尝试访问 name 属性会导致编译错误。
  • 不过,通过 greet 方法可以间接访问 name,因为 greetPerson 类的内部方法,内部方法可以访问类的私有成员。

注意:

  • 在 TypeScript 3.8 版本引入了 # 私有字段,这是一种新的语法,同样用于表示私有属性,不过这是一个运行时功能,而不仅仅是编译阶段的检查。例如:
class Person {
  #age: number;

  constructor(age: number) {
    this.#age = age;
  }

  growOlder() {
    this.#age++;
  }
}

let person = new Person(30);
person.growOlder();
console.log(person.#age); // 错误: 私有字段在类 'Person' 外部不可访问。
  • 在这个例子中,#age 是一个私有字段,只能在 Person 类的内部被访问和修改。尝试在类的外部访问或者修改这个字段将会导致运行时错误。

理解 protected

在 TypeScript 中,protected修饰符用于类的属性和方法,它介于public(公共)和private(私有)之间。使用protected修饰符,可以确保成员不会被外部访问,而只能在类及其子类中被访问。

  • 基本概念:

    • public成员在任何地方都可以自由访问。
    • private成员只能被包含它们的类访问。
    • protected成员可以被包含它们的类以及所有派生自这个类的子类访问。
  • 例子 1:类中使用 protected

    class Person {
      protected name: string;
    
      constructor(name: string) {
        this.name = name;
      }
    }
    
    class Employee extends Person {
      private department: string;
    
      constructor(name: string, department: string) {
        super(name);
        this.department = department;
      }
    
      public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
      }
    }
    
    let howard = new Employee("Howard", "Sales");
    console.log(howard.getElevatorPitch());
    // console.log(howard.name); // 错误: 'name' 属性是受保护的
    

    在这个例子中,name属性是受保护的,所以只能在Person类及其子类Employee中被使用。尝试直接访问howard.name将导致编译错误,因为该属性在Employee外部是不可见的。

  • 例子 2:构造函数也可以标记为 protected

    class Person {
      protected name: string;
      protected constructor(name: string) {
        this.name = name;
      }
    }
    
    // Employee can extend Person
    class Employee extends Person {
      private department: string;
    
      constructor(name: string, department: string) {
        super(name);
        this.department = department;
      }
    }
    
    // Error: The 'Person' constructor is protected
    // let john = new Person("John");
    
    let howard = new Employee("Howard", "Sales");
    

    在这里,Person的构造函数被标记为protected。这意味着你不能从Person类外部实例化对象,但你可以从它的子类Employee中调用它,允许子类实例化。

通过这些例子,你可以看到protected修饰符是如何工作的,以及它如何帮助我们控制对类成员的访问权限,同时允许继承的类使用这些成员。

readonly 修饰符

  • readonly修饰符用在 TypeScript 中的类属性前,表示该属性为只读,即一旦被初始化之后就不能再被修改。
  • 在实践中,使用readonly可以确保类的某些关键属性在创建对象后不会被意外改变,这对于保持数据的不可变性和编程的安全性非常有帮助。

示例:

class Person {
  readonly name: string;

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

  changeName(newName: string) {
    // 错误!name是只读属性,不能修改
    // this.name = newName;
  }
}

const person = new Person("Alice");
console.log(person.name); // 输出 "Alice"
// person.name = "Bob";  // 错误!不能修改只读属性
  • 在上面的例子中,Person类有一个readonly属性name。在构造函数中我们可以给它赋值,但之后尝试修改name属性将会导致编译时错误。
  • readonly也经常用在 TypeScript 的接口中,来指定某个属性不能被更改。

示例:

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

let p1: Point = { x: 10, y: 20 };
// p1.x = 5; // 错误!x是只读属性,不能修改
  • 这个例子中定义了一个包含readonly属性的接口Point。一旦Point类型的对象p1被创建并初始化,它的xy属性就不能被修改了。

参数属性

参数属性是 TypeScript 提供的一种简洁语法,用于在一个地方创建并初始化一个成员变量。通常,在类中定义成员变量以及通过构造函数接收参数时,必须分开声明和初始化,这样看起来代码会比较冗长。通过使用参数属性,你可以直接在构造器的参数前添加一个访问限定符(public, private, protected, 或 readonly),这样就能同时完成声明和初始化。

参数属性与readonly修饰符结合使用时,它不仅简化了成员的创建,还设置了成员的只读属性。readonly修饰符确保类的属性在初始化后不能被修改(只能在声明时或构造函数里进行初始化)。

  • 使用readonly修饰符标记类属性为只读。
  • 在构造函数参数前使用访问修饰符(如public, private等)将参数直接转换为类属性,无需额外声明。
  • 参数属性通过在构造函数中直接给参数加上访问修饰符,简化了成员变量的创建。

下面是一个使用readonly和参数属性的例子:

class Animal {
  constructor(readonly name: string) {}

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

let cat = new Animal("Whiskers");
cat.move(5);
// Output: Whiskers moved 5m.
// 下面这行会抛出编译时错误,因为name是只读属性
// cat.name = "Daisy"; // error: Cannot assign to 'name' because it is a read-only property.

在这个例子中,Animal 类有一个只读属性name,该属性通过构造函数的参数属性初始化。我们尝试修改name属性时,TypeScript 编译器会给出错误信息,因为name是只读的。

存取器

存取器(Accessors)是 TypeScript 中的一种特殊类型的方法,它们允许你控制对一个对象属性的访问。具体来说,存取器分为两种:

  • getter:用于获取属性值
  • setter:用于设置属性值

使用存取器可以提供比简单的属性更复杂的读写逻辑,比如在设置属性值之前进行验证或者在读取属性值时进行计算。

以下是一些关于存取器的要点和实例:

  • getter 和 setter 方法允许你对成员变量的读取和赋值进行控制。
  • 通过使用get关键字定义 getter 方法,使用set关键字定义 setter 方法。
  • 存取器通常用于需要对属性进行额外处理的场景。
class Employee {
  private _fullName: string = "";

  get fullName(): string {
    return this._fullName;
  }

  set fullName(newName: string) {
    if (newName && newName.length > 3) {
      this._fullName = newName;
    } else {
      console.log("Error: 名称至少需要3个字符。");
    }
  }
}

let employee = new Employee();
employee.fullName = "John Doe"; // 正确,名称长度符合要求
console.log(employee.fullName); // 输出: John Doe

employee.fullName = "Jo"; // 错误,名称长度不符合要求
// 控制台输出: Error: 名称至少需要3个字符。

注意:

  • 存取器要求编译选项target设置为 ES5 以上,因为 ES3 不支持存取器。
  • 只带有 get 没有 set 的存取器自动被推断为 readonly,意味着这个属性只能读不能写。
  • 如果你的类中有 setter,它应该与 getter 拥有相同的类型。

静态属性

静态属性指的是类本身拥有的属性,而不是类的实例对象拥有的属性。在 JavaScript 中,你可能已经见过构造函数的属性和方法,它们可以直接通过构造函数来访问,而不需要创建一个新的实例。在 TypeScript 中,静态属性用static关键字标记。

  • 静态属性通常用于存储类级别的数据,或者作为工具函数,这些数据和函数与类的任何实例无关。
  • 在 TypeScript 中,你可以在类内部使用static关键字定义静态属性。这样的属性不会出现在类的实例上,而是直接通过类名来访问。

以下是一些静态属性的例子:

class Point {
  static origin = new Point(0, 0);

  constructor(public x: number, public y: number) {}
}

console.log(Point.origin); // 访问静态属性,输出:Point { x: 0, y: 0 }

在这个例子中:

  • Point类有一个静态属性origin,表示坐标原点。
  • 创建了Point的实例时,每个实例都将有自己的xy属性,但是origin属性是所有实例共享的,并且可以通过Point.origin来访问,而不是通过某个特定的Point实例。

再看一个使用静态属性作为计数器的例子:

class Dog {
  static numDogs: number = 0;

  constructor(public name: string) {
    Dog.numDogs++;
  }

  static howManyDogs(): number {
    return Dog.numDogs;
  }
}

const dog1 = new Dog("Buddy");
const dog2 = new Dog("Charlie");

console.log(Dog.howManyDogs()); // 输出:2,因为我们创建了两个Dog实例

在这个例子中:

  • Dog类有一个静态属性numDogs,用于跟踪创建了多少个Dog实例。
  • 每次创建Dog的新实例时,构造函数都会增加numDogs的值。
  • howManyDogs是一个静态方法,它返回当前创建的Dog实例数量。
  • 注意,我们通过Dog.numDogsDog.howManyDogs()来访问静态属性和方法,而不是通过一个Dog实例。

抽象类

抽象类是一种特殊的类,无法直接实例化。在 TypeScript 中,它们通常用作其他派生类的基类。使用关键字abstract来定义一个抽象类或抽象方法。抽象类中的抽象方法必须在派生类中被实现。

  • 抽象类的特点:

    • 不能直接实例化,只能被继承。
    • 可以包含实现细节的成员和必须由派生类实现的抽象方法。
    • 抽象方法只有签名,没有方法体。
  • 使用场景:

    • 当你想创建一个类来定义其他类应遵循的规范时,如特定的方法和属性。
    • 当你希望提供一些通用功能,同时强制派生类实现特定的方法时。
abstract class Animal {
  abstract makeSound(): void; // 抽象方法,无具体实现

  move(): void {
    // 非抽象方法,有具体实现
    console.log("roaming the earth...");
  }
}

class Dog extends Animal {
  makeSound(): void {
    // 必须实现抽象方法
    console.log("bark bark");
  }
}

const myDog = new Dog();
myDog.makeSound(); // 输出: bark bark
myDog.move(); // 输出: roaming the earth...

// const myAnimal = new Animal(); // 错误: 不能创建抽象类的实例

在这个例子中,我们定义了一个抽象类Animal,它有一个抽象方法makeSound和一个具体方法move。然后我们创建了一个继承自Animal的派生类Dog,并实现了makeSound方法。尝试直接实例化抽象类Animal会导致编译错误,因为抽象类不允许直接被实例化。

高级技巧

在 TypeScript 中,使用instanceof进行类型保护是一个高级技巧,允许你在运行时检查一个对象是否为某个特定类或其子类的实例。这种方法常被用于判断具体的实例类型,并且可以帮助 TypeScript 编译器理解条件代码块内的类型,从而能够提供更准确的类型推断和错误检查。

  • 基础用法:

    • 假设有一个基类Animal和两个派生类CatDogCatDog都扩展了Animal,但是它们各自实现了不同的方法,比如meow()只存在于Cat中,而bark()只存在于Dog中。
    • 当我们在函数中接收到一个Animal类型的参数,并希望根据不同的动物类型调用相应的方法时,可以使用instanceof来区分。
    class Animal {}
    class Cat extends Animal {
      meow() {
        console.log("meow");
      }
    }
    class Dog extends Animal {
      bark() {
        console.log("bark");
      }
    }
    
    function makeSound(animal: Animal) {
      if (animal instanceof Cat) {
        animal.meow();
      } else if (animal instanceof Dog) {
        animal.bark();
      }
    }
    
    • 在上面的例子中,makeSound函数接受一个类型为Animal的参数。通过使用instanceof检查,我们可以确定传入的具体类型,并安全地调用特定于该类型的方法(如meowbark)。这是因为instanceof告诉 TypeScript 编译器:“在这个条件块中,animal是一个Cat/Dog实例”,使得编译器可以正确地推断出方法调用是安全的。
  • 类型保护的优势:

    • 使用instanceof进行类型保护不仅可以在运行时检查类型,还改善了 TypeScript 的静态类型检查。通过明确地指示变量属于哪个具体类,可以大大减少类型相关的错误,提高代码的健壮性。
    • 它允许开发者编写出既清晰又安全的类型条件逻辑,而无需频繁使用类型断言或直接操作类型信息。

记住,尽管instanceof是处理类实例类型的强大工具,但应当谨慎使用,尤其是在设计需要广泛的类型兼容性的系统时。过度依赖instanceof可能会降低代码的灵活性和可维护性。

构造函数

在 TypeScript 中,构造函数(Constructor)是类创建实例时调用的特殊方法。但当我们讨论构造函数这一“高级技术”时,通常指的是与新建对象实例有关的模式和技巧,尤其是如何定义一个类的类型以及如何处理类和接口之间的关系。下面通过几个例子详细解释这个概念。

  • 使用类作为接口

    类在 TypeScript 中不仅仅是创建对象的蓝图,还可以作为类型使用。这意味着你可以使用一个类来描述一个对象的形状。这对于定义构造函数类型特别有用。

class Car {
  constructor(public make: string, public model: string) {}
}

let carCreator: new (make: string, model: string) => Car;
carCreator = Car; // 正确

let myCar = new carCreator("Ford", "Fiesta");

这里,new (make: string, model: string) => Car 描述了一个接受两个字符串参数并返回一个 Car 类型实例的构造函数类型。

  • 在类和接口之间建立关系

    TypeScript 允许使用接口来明确地约束一个类必须实现的构造函数形状。

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

interface ClockInterface {
  tick(): void;
}

function createClock(
  ctor: ClockConstructor,
  hour: number,
  minute: number
): ClockInterface {
  return new ctor(hour, minute);
}

class DigitalClock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
    console.log("beep beep");
  }
}

class AnalogClock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
    console.log("tick tock");
  }
}

let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);

在这个例子中,ClockConstructor 接口定义了一个构造函数签名。它规定了任何想要被 createClock 函数接受的构造函数都必须遵循这个签名。这样做允许 createClock 函数保持高度的灵活性,因为它可以接受任何符合该构造函数签名的类。

通过以上两种方式,TypeScript 的构造函数技巧使得在定义类和相关对象时提供了极大的灵活性和强大的类型安全。

把类当做接口使用

在 TypeScript 中,类不仅仅是创建对象的模板,它们还可以作为定义对象形状的接口使用。这意味着你可以使用类来描述一个对象应该有哪些属性和方法,而不一定要用这个类去实例化对象。使用类作为接口可以在某些情况下提供更多灵活性。

  • 类型定义和实现分离

    • 通常,当我们定义一个接口时,我们只关心对象的结构,而不关心对象的具体实现。
    • 类可以实现接口,但同时也可以包含具体实现的细节。
    • 当你想要一个对象既有具体实现,又要表达其类型时,可以将类当做接口使用。
  • 实用例子

    • 假设你有一个类Point,它有两个属性:xy,以及一个方法moveTo

      class Point {
        x: number;
        y: number;
      
        constructor(x: number, y: number) {
          this.x = x;
          this.y = y;
        }
      
        moveTo(newX: number, newY: number) {
          this.x = newX;
          this.y = newY;
        }
      }
      
    • 现在,你希望定义一个函数,它接收一个有xy属性和moveTo方法的对象。

      • 你可以直接使用Point类作为参数的类型。
      function moveSomething(p: Point) {
        p.moveTo(0, 0);
      }
      
    • 这里,Point类就被当做了一个接口,尽管我们并没有显式地定义一个接口。

  • 类静态部分与实例部分分开

    • 需要注意的是,在 TypeScript 中,类有两个部分:静态部分和实例部分。
    • 当你使用类作为接口时,你只能描述类的实例部分的结构,不能描述静态部分。
    • 如果你需要描述静态部分,你应该单独定义一个接口。
  • 示例展示把类当成接口使用

    • 假设你想要一个函数,它可以处理任何拥有serialize方法的类实例。
    • 你先定义一个带有serialize的类:
      class Serializable {
        serialize() {
          // ...返回序列化对象的逻辑
        }
      }
      
    • 然后,你可以写一个接收这个类实例的函数,并使用这个类作为类型约束:
      function save(obj: Serializable) {
        const serializedData = obj.serialize();
        // ...保存序列化数据的逻辑
      }
      
    • 在这个例子中,Serializable类的作用就像是定义了一个具有serialize方法的接口,可以保证传入save函数的对象都会有这个方法。

通过把类当作接口使用,TypeScript 用户可以更加自然地在声明类型时重用现有的类结构,同时也保持了代码的整洁和可维护性。