Skip to content

Class

TypeScript 完全支持 ES2015 中引入的 class 关键字。

类型的特性(成员)

领域

字段声明在类上创建一个公共可写属性:

typescript
class Point {
  x: number;
  y: number;
}
const pt = new Point();
pt.x = 0;
pt.y = 0;
class Point {
  x: number;
  y: number;
}
const pt = new Point();
pt.x = 0;
pt.y = 0;

与其他位置一样,类型注释是可选的,但如果未指定,则为隐式 any。

字段也可以具有初始值设定项;这些将在类实例化时自动运行:

typescript
class Point {
  x = 0;
  y = 0;
}
const pt = new Point();
console.log(`${pt.x}, ${pt.y}`);
class Point {
  x = 0;
  y = 0;
}
const pt = new Point();
console.log(`${pt.x}, ${pt.y}`);
和const、let 和 var 一样,类属性的初始化器将用于推断其类型
typescript
class Point {
  x = 0; // 推断其类型为number
  y = 0; // 推断其类型为number
}
const pt = new Point();
pt.x = "0";
// 报错:Type 'string' is not assignable to type 'number'.
class Point {
  x = 0; // 推断其类型为number
  y = 0; // 推断其类型为number
}
const pt = new Point();
pt.x = "0";
// 报错:Type 'string' is not assignable to type 'number'.

配置 strictPropertyInitialization 控制是否需要在构造函数中初始化类字段。

typescript
// 如果配置了strictPropertyInitialization 下面的name会报错
class BadGreeter {
  name: string; // Property 'name' has no initializer and is not definitely assigned in the constructor.
  // declare name: string; // 可以使用 declare 解决未在 constructor 中使用的问题。
}

// 配置了strictPropertyInitialization 就必须为其在constructor中明确分配。如下:
class BadGreeter {
  name: string;
  constructor() {
    this.name = "hello";
  }
}
// 如果配置了strictPropertyInitialization 下面的name会报错
class BadGreeter {
  name: string; // Property 'name' has no initializer and is not definitely assigned in the constructor.
  // declare name: string; // 可以使用 declare 解决未在 constructor 中使用的问题。
}

// 配置了strictPropertyInitialization 就必须为其在constructor中明确分配。如下:
class BadGreeter {
  name: string;
  constructor() {
    this.name = "hello";
  }
}

请注意,需要在构造函数本身中初始化该字段。TypeScript 不会分析您从构造函数调用的方法来检测初始化,因为派生类可能会覆盖这些方法并且无法初始化成员。

如果你打算通过构造函数以外的方式明确地初始化一个字段(例如,可能一个外部库正在为你填充类的一部分),你可以使用明确赋值断言运算符!

typescript
class OKGreeter {
  name!: string; // 使用非空断言
}
class OKGreeter {
  name!: string; // 使用非空断言
}

只读

字段可以带有 readonly 修饰符的前缀。这可以防止对构造函数外部的字段进行赋值。

typescript
class Greeter {
  readonly name: string = "world";
  constructor(otherName?: string) {
    if (otherName !== undefined) {
      this.name = otherName;
    }
  }
  err() {
    this.name = "not ok"; // Cannot assign to 'name' because it is a read-only property.
  }
}
const g = new Greeter();
g.name = "also not ok";   // Cannot assign to 'name' because it is a read-only property.
class Greeter {
  readonly name: string = "world";
  constructor(otherName?: string) {
    if (otherName !== undefined) {
      this.name = otherName;
    }
  }
  err() {
    this.name = "not ok"; // Cannot assign to 'name' because it is a read-only property.
  }
}
const g = new Greeter();
g.name = "also not ok";   // Cannot assign to 'name' because it is a read-only property.

构造函数

class中的构造函数与函数非常相似。您可以添加带有类型注释、默认值和重载的参数:

typescript
class Point {
  x: number = 0;
  y: number = 0;
  // Constructor overloads //不建议这样做
  constructor(x: number, y: number);
  constructor(xy: string);
  constructor(x: string | number, y: number = 0) {
    // Code logic here
  }
}
class Point {
  x: number = 0;
  y: number = 0;
  // Constructor overloads //不建议这样做
  constructor(x: number, y: number);
  constructor(xy: string);
  constructor(x: string | number, y: number = 0) {
    // Code logic here
  }
}

class中的构造函数签名和函数签名之间只有一些区别:

  • 构造函数不能有类型参数 - 这些参数属于外部类声明,我们稍后会了解这一点
  • 构造函数不能有返回类型注释 - 类实例类型始终是返回的内容

super

在继承的class中(基类),需要在构造函数主体中调用 super(),且必须在构造函数顶部使用(必须在this之前)。

typescript
class Base {
  k = 4;
}
class Derived extends Base {
  constructor() {
    console.log(this.k);
    super();
  }
}
// 报错:'super' must be called before accessing 'this' in the constructor of a derived class.
class Base {
  k = 4;
}
class Derived extends Base {
  constructor() {
    console.log(this.k);
    super();
  }
}
// 报错:'super' must be called before accessing 'this' in the constructor of a derived class.

方法

类的 function 属性称为 method。方法可以使用与函数和构造函数相同的-类型注释(入参/返回类型):

typescript
class Point {
  x = 10;
  y = 10;
  scale(n: number): void {
    this.x *= n;
    this.y *= n;
  }
}
class Point {
  x = 10;
  y = 10;
  scale(n: number): void {
    this.x *= n;
    this.y *= n;
  }
}

请注意,在方法体中,仍然必须通过 this 访问字段和其他方法。方法体中的非限定名称将始终引用封闭作用域中的某个内容:

typescript
let x: number = 0;
class C {
  x: string = "hello";
  m() {
    x = "world"; 
    // 报错:Type 'string' is not assignable to type 'number'.
    // 应为:这里的 x 是外面的那个x
  }
}
let x: number = 0;
class C {
  x: string = "hello";
  m() {
    x = "world"; 
    // 报错:Type 'string' is not assignable to type 'number'.
    // 应为:这里的 x 是外面的那个x
  }
}

getter/setter

类的访问器:

typescript
class C {
  _length = 0;
  get length() {
    return this._length;
  }
  set length(value) {
    this._length = value;
  }
}
class C {
  _length = 0;
  get length() {
    return this._length;
  }
  set length(value) {
    this._length = value;
  }
}

TypeScript 对访问器有一些特殊的推理规则:

  • 如果 get 存在但未设置,则该属性自动为 readonly
  • 如果未指定 setter 参数的类型,则从 getter 的返回类型推断出该类型。

从 TypeScript 4.3 开始,可以使用不同类型的访问器来获取和设置。

typescript
class Thing {
  _size = 0;
  get size(): number {
    return this._size;
  }
  set size(value: string | number | boolean) {
    let num = Number(value);
    if (!Number.isFinite(num)) {
      this._size = 0;
      return;
    }
    this._size = num;
  }
}
class Thing {
  _size = 0;
  get size(): number {
    return this._size;
  }
  set size(value: string | number | boolean) {
    let num = Number(value);
    if (!Number.isFinite(num)) {
      this._size = 0;
      return;
    }
    this._size = num;
  }
}

索引签名

类可以声明索引签名;这些方法与其他对象类型的 Index Signatures 相同:

typescript
class MyClass {
  [s: string]: boolean | ((s: string) => boolean);
  check(s: string) {
    return this[s] as boolean;
  }
}
class MyClass {
  [s: string]: boolean | ((s: string) => boolean);
  check(s: string) {
    return this[s] as boolean;
  }
}

由于索引签名类型还需要捕获方法类型,因此不容易有效地使用这些类型。通常,最好将索引数据存储在另一个位置,而不是 class 实例本身。

继承

implements

可以使用 implements 子句来检查类是否满足特定接口。如果类未能正确实现它,则会发出错误:

typescript
interface Pingable {
  ping(): void;
}
class Sonar implements Pingable {
  ping() {
    console.log("ping!");
  }
}
class Ball implements Pingable {
  pong() {
    console.log("pong!");
  }
}
// Class 'Ball' incorrectly implements interface 'Pingable'.
// Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.
interface Pingable {
  ping(): void;
}
class Sonar implements Pingable {
  ping() {
    console.log("ping!");
  }
}
class Ball implements Pingable {
  pong() {
    console.log("pong!");
  }
}
// Class 'Ball' incorrectly implements interface 'Pingable'.
// Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.

类也可以实现多个接口,例如类 C implements A、B {}

请务必了解 implements 子句只是检查类是否可以被视为接口类型。它根本不会更改类或其方法的类型。

typescript
interface Checkable {
  check(name: string): boolean;      // 这里为check入参为string
}
class NameChecker implements Checkable {
  check(s) {
    return s.toLowerCase() === "ok"; // 这里为check入参 s 类型为any,所以会报错
  }
}
interface Checkable {
  check(name: string): boolean;      // 这里为check入参为string
}
class NameChecker implements Checkable {
  check(s) {
    return s.toLowerCase() === "ok"; // 这里为check入参 s 类型为any,所以会报错
  }
}

一个常见的错误来源是假设 implements 子句会改变类类型 - 它不会! 在这个例子中,我们可能预料到 s 的类型会受到 check 的 name: string 参数的影响。事实并非如此 - implements 子句不会更改检查类体或推断类类型的方式。

extends

类可以从基类扩展而来。派生类具有其基类的所有属性和方法,还可以定义其他成员。

typescript
class Animal {
  move() {
    console.log("Moving along!");
  }
}
class Dog extends Animal {
  woof(times: number) {
    for (let i = 0; i < times; i++) {
      console.log("woof!");
    }
  }
}
const d = new Dog();
d.move();
d.woof(3);
class Animal {
  move() {
    console.log("Moving along!");
  }
}
class Dog extends Animal {
  woof(times: number) {
    for (let i = 0; i < times; i++) {
      console.log("woof!");
    }
  }
}
const d = new Dog();
d.move();
d.woof(3);

派生类还可以重写基类字段或属性。可以使用 super. 语法来访问基类方法。

typescript
class Base {
  greet() {
    console.log("Hello, world!");
  }
}
class Derived extends Base {
  greet(name?: string) {
    if (name === undefined) {
      super.greet();
    } else {
      console.log(`Hello, ${name.toUpperCase()}`);
    }
  }
  // name 参数在 Base 没有定义,直接
  // greet(name: string) {
  //   console.log(`Hello, ${name.toUpperCase()}`);
  // }
}
const d = new Derived();
d.greet();
d.greet("reader");
class Base {
  greet() {
    console.log("Hello, world!");
  }
}
class Derived extends Base {
  greet(name?: string) {
    if (name === undefined) {
      super.greet();
    } else {
      console.log(`Hello, ${name.toUpperCase()}`);
    }
  }
  // name 参数在 Base 没有定义,直接
  // greet(name: string) {
  //   console.log(`Hello, ${name.toUpperCase()}`);
  // }
}
const d = new Derived();
d.greet();
d.greet("reader");

派生类必须遵循其基类协定,这一点很重要。请记住,通过基类引用引用派生类实例是很常见的(并且始终是合法的):

typescript
const b: Base = d;
b.greet(); // ok 的
const b: Base = d;
b.greet(); // ok 的

declare

target >= ES2022 或 配置useDefineForClassFields 为 true 时:

  1. 类字段将在父类构造函数完成后初始化,并覆盖父类设置的任何值。
  2. 当您只想为继承的字段重新声明更准确的类型时,这可能是一个问题。
  3. 要处理这些情况,您可以编写 declare 来向 TypeScript 指示此字段声明不应有运行时效果。
typescript
interface Animal {
  dateOfBirth: any;
}
interface Dog extends Animal {
  breed: any;
}
class AnimalHouse {
  resident: Animal;
  constructor(animal: Animal) {
    this.resident = animal;
  }
}
class DogHouse extends AnimalHouse {
  declare resident: Dog;  // declare 指示此字段不应有运行时效果。
  constructor(dog: Dog) {
    super(dog);
  }
}
interface Animal {
  dateOfBirth: any;
}
interface Dog extends Animal {
  breed: any;
}
class AnimalHouse {
  resident: Animal;
  constructor(animal: Animal) {
    this.resident = animal;
  }
}
class DogHouse extends AnimalHouse {
  declare resident: Dog;  // declare 指示此字段不应有运行时效果。
  constructor(dog: Dog) {
    super(dog);
  }
}

初始化顺序

在某些情况下,JavaScript 类的初始化顺序可能会令人惊讶。让我们考虑一下这段代码:

typescript
class Base {
  name = "base";
  constructor() {
    console.log("My name is " + this.name);
  }
}
class Derived extends Base {
  name = "derived";
}
const d = new Derived();
class Base {
  name = "base";
  constructor() {
    console.log("My name is " + this.name);
  }
}
class Derived extends Base {
  name = "derived";
}
const d = new Derived();

由 JavaScript 定义的类初始化顺序为:

  1. 初始化基类字段
  2. 基类构造函数运行
  3. 初始化派生类字段
  4. 派生类构造函数运行

这意味着基类构造函数在其自己的构造函数期间看到了自己的 name 值,因为派生类字段初始化尚未运行。

继承内置类型 ()

注意:高版本不可以不用看该知识点

如果你不打算继承 Array、Error、Map 等内置类型,或者你的编译目标被显式设置为 ES6/ES2015 或更高版本,你可以跳过本节

在 ES2015 中,返回对象的构造函数隐式地将 this 的值替换为 super(...) 的任何调用方。生成的构造函数代码必须捕获 super(...) 的任何潜在返回值并将其替换为 this。

因此,子类化 Error、Array 和其他子类化可能不再按预期工作。这是因为 Error、Array 等的构造函数使用 ECMAScript 6 的 new.target 来调整原型链;但是,在 ECMAScript 5 中调用构造函数时,无法确保 new.target 的值。默认情况下,其他下层编译器通常具有相同的限制。

对于如下所示的子类:

typescript
class MsgError extends Error {
  constructor(m: string) {
    super(m);
  }
  sayHello() {
    return "hello " + this.message;
  }
}
class MsgError extends Error {
  constructor(m: string) {
    super(m);
  }
  sayHello() {
    return "hello " + this.message;
  }
}

您可能会发现:

  1. methods 可能在构造这些子类返回的对象上未定义,因此调用 sayHello 将导致错误。
  2. instanceof 将在子类的实例及其实例之间断开,因此 (new MsgError()) instanceof MsgError 将返回 false。

作为建议,您可以在任何 super(...) 调用后立即手动调整原型。

typescript
class MsgError extends Error {
  constructor(m: string) {
    super(m);
    // 在super(...)后,手动设置原型 (不设置在es5中会有问题,es6没问题)
    Object.setPrototypeOf(this, MsgError.prototype);
  }
  sayHello() {
    return "hello " + this.message;
  }
}
class MsgError extends Error {
  constructor(m: string) {
    super(m);
    // 在super(...)后,手动设置原型 (不设置在es5中会有问题,es6没问题)
    Object.setPrototypeOf(this, MsgError.prototype);
  }
  sayHello() {
    return "hello " + this.message;
  }
}

MsgError 的任何子类也必须手动设置原型。对于不支持 Object.setPrototypeOf 的运行时,您可以改用 __proto__

遗憾的是,这些解决方法不适用于 Internet Explorer 10 及更早版本。可以手动将方法从原型复制到实例本身(即 MsgError.prototype 复制到 this 上),但原型链本身无法修复。

可见性

public

typescript
class Greeter {
  public greet() {
    console.log("hi!");
  }
}
const g = new Greeter();
g.greet();
class Greeter {
  public greet() {
    console.log("hi!");
  }
}
const g = new Greeter();
g.greet();

由于 public 已经是默认的可见性修饰符,因此您永远不需要在类成员上编写它,但出于样式/可读性原因,可能会选择这样做。

protected

protected 成员仅对声明它们的类的子类可见

typescript
class Greeter {
  public greet() {
    console.log("Hello, " + this.getName());
  }
  protected getName() {
    return "hi";
  }
}
class SpecialGreeter extends Greeter {
  public howdy() {
    // 可以在此处访问受保护的成员
    console.log("Howdy, " + this.getName());
  }
}
const g = new SpecialGreeter();
g.greet(); // OK
g.getName(); // Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.
class Greeter {
  public greet() {
    console.log("Hello, " + this.getName());
  }
  protected getName() {
    return "hi";
  }
}
class SpecialGreeter extends Greeter {
  public howdy() {
    // 可以在此处访问受保护的成员
    console.log("Howdy, " + this.getName());
  }
}
const g = new SpecialGreeter();
g.greet(); // OK
g.getName(); // Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.

受保护成员的暴露

派生类需要遵循其基类协定,但可以选择公开具有更多功能的基类的子类型。这包括将受保护的成员设为公共:

typescript
class Base {
  protected m = 10;
}
class Derived extends Base {
  // 没有修饰符,所以默认是 'public'
  m = 15;
}
const d = new Derived();
console.log(d.m); // OK
class Base {
  protected m = 10;
}
class Derived extends Base {
  // 没有修饰符,所以默认是 'public'
  m = 15;
}
const d = new Derived();
console.log(d.m); // OK

跨层次结构保护访问

TypeScript 不允许访问类层次结构中同级类的受保护成员:

typescript
class Base {
  protected x: number = 1;
}
class Derived1 extends Base {
  protected x: number = 5;
}
class Derived2 extends Base {
  f1(other: Derived2) {
    other.x = 10;
  }
  f2(other: Derived1) {
    other.x = 10; // Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.
  }
}
class Base {
  protected x: number = 1;
}
class Derived1 extends Base {
  protected x: number = 5;
}
class Derived2 extends Base {
  f1(other: Derived2) {
    other.x = 10;
  }
  f2(other: Derived1) {
    other.x = 10; // Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.
  }
}

这是因为访问 Derived2 中的 x 应该只能从 Derived2 的子类中合法,而 Derived1 不是其中之一。此外,如果通过 Derived1 引用访问 x 是非法的(这当然应该是非法的),那么通过基类引用访问它应该永远不会改善这种情况。

private

private 与 protected 类似,但不允许访问成员,即使从子类也是如此:

typescript
class Base {
  private x = 0;
}
const b = new Base();
console.log(b.x); // 报错:Property 'x' is private and only accessible within class 'Base'.

class Derived extends Base {
  showX() {
    console.log(this.x); //报错: Property 'x' is private and only accessible within class 'Base'.
  }
}
class Base {
  private x = 0;
}
const b = new Base();
console.log(b.x); // 报错:Property 'x' is private and only accessible within class 'Base'.

class Derived extends Base {
  showX() {
    console.log(this.x); //报错: Property 'x' is private and only accessible within class 'Base'.
  }
}

由于私有成员对派生类不可见,因此派生类无法提高其可见性:

typescript
class Base {
  private x = 0;
}
class Derived extends Base {
  // Class 'Derived' incorrectly extends base class 'Base'.
  // Property 'x' is private in type 'Base' but not in type 'Derived'.
  x = 1;
}
class Base {
  private x = 0;
}
class Derived extends Base {
  // Class 'Derived' incorrectly extends base class 'Base'.
  // Property 'x' is private in type 'Base' but not in type 'Derived'.
  x = 1;
}

跨实例私有访问:

不同的 OOP 语言对于同一类的不同实例是否可以访问彼此的私有成员存在分歧。虽然 Java、C#、C++、Swift 和 PHP 等语言允许这样做,但 Ruby 不允许

TypeScript 允许跨实例私有访问:

typescript
class A {
  private x = 10;
  public sameAs(other: A) {
    // No error
    return other.x === this.x;
  }
}
class A {
  private x = 10;
  public sameAs(other: A) {
    // No error
    return other.x === this.x;
  }
}

与 TypeScript 类型系统的其他方面一样,private 和 protected仅在类型检查期间强制执行。

这意味着 JavaScript 运行时构造(如 in 或简单属性查找)仍然可以访问 private 或 protected 成员:

typescript
class MySafe {
  private secretKey = 12345;
}

// In a JavaScript file...
const s = new MySafe();
// Will print 12345
console.log(s.secretKey);
class MySafe {
  private secretKey = 12345;
}

// In a JavaScript file...
const s = new MySafe();
// Will print 12345
console.log(s.secretKey);

private 还允许在类型检查期间使用括号表示法进行访问。这使得 private-declared 的字段可能更容易用于单元测试等作,但缺点是这些字段是软 private 的,并且不严格执行隐私。

typescript
class MySafe {
  private secretKey = 12345;
}
 
const s = new MySafe();
 
// Not allowed during type checking
console.log(s.secretKey);
// Property 'secretKey' is private and only accessible within class 'MySafe'.
 
// OK
console.log(s["secretKey"]);
class MySafe {
  private secretKey = 12345;
}
 
const s = new MySafe();
 
// Not allowed during type checking
console.log(s.secretKey);
// Property 'secretKey' is private and only accessible within class 'MySafe'.
 
// OK
console.log(s["secretKey"]);

与 TypeScript 的私有不同,JavaScript 的私有字段 # 在编译后仍然是私有的,并且不提供前面提到的转义舱口(如括号表示法访问),这使得它们成为硬私有。

typescript
class Dog {
  #barkAmount = 0;
  personality = "happy";
  constructor() {}
}
// 编译
"use strict";
class Dog {
    #barkAmount = 0;
    personality = "happy";
    constructor() { }
}
// 当编译为 ES2021 或更低版本时,TypeScript 将使用 WeakMaps 代替 #。"use strict";
var _Dog_barkAmount;
class Dog {
    constructor() {
        _Dog_barkAmount.set(this, 0);
        this.personality = "happy";
    }
}
_Dog_barkAmount = new WeakMap();
class Dog {
  #barkAmount = 0;
  personality = "happy";
  constructor() {}
}
// 编译
"use strict";
class Dog {
    #barkAmount = 0;
    personality = "happy";
    constructor() { }
}
// 当编译为 ES2021 或更低版本时,TypeScript 将使用 WeakMaps 代替 #。"use strict";
var _Dog_barkAmount;
class Dog {
    constructor() {
        _Dog_barkAmount.set(this, 0);
        this.personality = "happy";
    }
}
_Dog_barkAmount = new WeakMap();
如果需要保护类中的值免受恶意行为者的侵害,则应使用提供硬运行时隐私的机制,例如闭包、WeakMaps 或私有字段。请注意,在运行时添加的这些隐私检查可能会影响性能 。

静态属性

类可以具有 static 成员。这些成员不与类的特定实例关联。它们可以通过类 constructor 对象本身来访问:

typescript
class MyClass {
  static x = 0;
  static printX() {
    console.log(MyClass.x);
  }
}
console.log(MyClass.x);
MyClass.printX();
class MyClass {
  static x = 0;
  static printX() {
    console.log(MyClass.x);
  }
}
console.log(MyClass.x);
MyClass.printX();

静态成员还可以使用相同的 publicprotectedprivate 可见性修饰符:

typescript
class MyClass {
  private static x = 0;
}
console.log(MyClass.x);
// 报错:Property 'x' is private and only accessible within class 'MyClass'.
class MyClass {
  private static x = 0;
}
console.log(MyClass.x);
// 报错:Property 'x' is private and only accessible within class 'MyClass'.

静态成员也是继承的:

typescript
class Base {
  static getGreeting() {
    return "Hello world";
  }
}
class Derived extends Base {
  myGreeting = Derived.getGreeting(); //ok
}
class Base {
  static getGreeting() {
    return "Hello world";
  }
}
class Derived extends Base {
  myGreeting = Derived.getGreeting(); //ok
}

特殊静态名称

它通常不安全/不可能覆盖 Function 原型中的属性。由于类本身是可以使用 new 调用的函数,因此不能使用某些静态名称。函数属性(如 namelengthcall)无法定义为静态成员:

typescript
class S {
  static name = "S!";
  // 报错:Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.
}
class S {
  static name = "S!";
  // 报错:Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.
}

静态块

静态块允许您编写具有自己的范围的语句序列,这些语句可以访问包含类中的私有字段。

这意味着我们可以编写具有编写语句的所有功能的初始化代码,没有变量泄漏,并且可以完全访问我们类的内部。

typescript
class Foo {
    static #count = 0;
    get count() {
        return Foo.#count;
    }
    static {
        try {
            const lastInstances = loadLastInstances();
            Foo.#count += lastInstances.length;
        }
        catch {}
    }
}
class Foo {
    static #count = 0;
    get count() {
        return Foo.#count;
    }
    static {
        try {
            const lastInstances = loadLastInstances();
            Foo.#count += lastInstances.length;
        }
        catch {}
    }
}

泛型类

类与接口非常相似,可以是泛型的。当使用 new 实例化泛型类时,其类型参数的推断方式与函数调用中的相同:

typescript
class Box<Type> {
  contents: Type;
  constructor(value: Type) {
    this.contents = value;
  }
}
const b = new Box("hello!");
class Box<Type> {
  contents: Type;
  constructor(value: Type) {
    this.contents = value;
  }
}
const b = new Box("hello!");

类可以像使用接口一样使用泛型 constraints 和 defaults。

typescript
class Box<Type> {
  static defaultValue: Type; // 报错:静态成员不能引用class类型参数。
}
class Box<Type> {
  static defaultValue: Type; // 报错:静态成员不能引用class类型参数。
}

请记住,类型总是被完全擦除的!在运行时,只有一个Box.defaultValue 属性槽。这意味着设置 Box<string>.defaultValue(如果可能的话)也会更改 Box<number>.defaultValue - 不好。泛型类的 static 成员永远不能引用类的类型参数

this 在类中的运行时

,而且 JavaScript 以具有一些奇特的运行时行为而闻名。JavaScript 对此的处理确实很不寻常:
typescript
class MyClass {
  name = "MyClass";
  getName() {
    return this.name;
  }
}
const c = new MyClass();
const obj = {
  name: "obj",
  getName: c.getName,
};
// Prints "obj", not "MyClass"
console.log(obj.getName()); // obj
class MyClass {
  name = "MyClass";
  getName() {
    return this.name;
  }
}
const c = new MyClass();
const obj = {
  name: "obj",
  getName: c.getName,
};
// Prints "obj", not "MyClass"
console.log(obj.getName()); // obj

和javascript一样,默认情况下,函数内部的 this 值取决于函数的调用方式。在此示例中,由于该函数是通过 obj 引用调用的,因此其 this 的值是 obj 而不是类实例。

这很少是你想要发生的事情!TypeScript 提供了一些方法来减轻或防止此类错误 -- 箭头函数。

箭头函数:

如果你有一个函数经常以丢失其 this 上下文的方式被调用,那么使用箭头函数属性而不是方法定义可能是有意义的:

typescript
class MyClass {
  name = "MyClass";
  getName = () => {
    return this.name;
  };
}
const c = new MyClass();
const g = c.getName;
console.log(g()); // MyClass
class MyClass {
  name = "MyClass";
  getName = () => {
    return this.name;
  };
}
const c = new MyClass();
const g = c.getName;
console.log(g()); // MyClass

使用箭头函数后,这有一些权衡:

  1. this 值保证在运行时是正确的,即使对于未使用 TypeScript 检查的代码也是如此
  2. 这将使用更多内存,因为每个类实例都将拥有以这种方式定义的每个函数的自己的副本
  3. 您不能在派生类中使用 super.getName,因为原型链中没有用于从中获取基类方法的条目

this 参数

在方法或函数定义中,名为 this 的初始参数在 TypeScript 中具有特殊含义。这些参数在编译过程中会被擦除:

typescript
// TypeScript input with 'this' parameter
function fn(this: SomeType, x: number) {
  /* ... */
}

// JavaScript output
function fn(x) {
  /* ... */
}
// TypeScript input with 'this' parameter
function fn(this: SomeType, x: number) {
  /* ... */
}

// JavaScript output
function fn(x) {
  /* ... */
}

TypeScript 会检查调用带有 this 参数的函数是否使用正确的上下文完成。我们可以在方法定义中添加 this 参数,而不是使用箭头函数,以静态强制正确调用该方法:

typescript
class MyClass {
  name = "MyClass";
  getName(this: MyClass) {
    return this.name;
  }
}
const c = new MyClass();
// OK
c.getName();
 
// Error, would crash
const g = c.getName;
console.log(g());
// 报错:The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.
class MyClass {
  name = "MyClass";
  getName(this: MyClass) {
    return this.name;
  }
}
const c = new MyClass();
// OK
c.getName();
 
// Error, would crash
const g = c.getName;
console.log(g());
// 报错:The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.

此方法对箭头函数方法进行了相反的权衡:

  1. JavaScript 调用者可能仍然会在没有意识到的情况下错误地使用 class method
  2. 每个类定义只分配一个函数,而不是每个类实例分配一个函数
  3. 基本方法定义仍然可以通过 super 调用。

this 的类型

在 类 中,名为 this 的特殊类型动态引用当前类的类型。让我们看看这有什么用:

typescript
class Box {
  contents: string = "";
  set(value: string) {
    this.contents = value;
    return this;
  }
}
class Box {
  contents: string = "";
  set(value: string) {
    this.contents = value;
    return this;
  }
}

在这里,TypeScript 将 set 的返回类型推断为 this,而不是 Box。

现在让我们创建一个 Box 的子类:

typescript
class ClearableBox extends Box {
  clear() {
    this.contents = "";
  }
}
const a = new ClearableBox();
const b = a.set("hello");
class ClearableBox extends Box {
  clear() {
    this.contents = "";
  }
}
const a = new ClearableBox();
const b = a.set("hello");

您还可以在 参数类型注解 中使用它:

typescript
class Box {
  content: string = "";
  sameAs(other: this) {
    return other.content === this.content;
  }
}
class Box {
  content: string = "";
  sameAs(other: this) {
    return other.content === this.content;
  }
}

这与编写 other: Box 不同 — 如果你有一个派生类,它的 sameAs 方法现在将只接受该相同派生类的其他实例:

typescript
class Box {
  content: string = "";
  sameAs(other: this) {
    return other.content === this.content;
  }
}
 
class DerivedBox extends Box {
  otherContent: string = "?";
}
 
const base = new Box();
const derived = new DerivedBox();
derived.sameAs(base); // base 报错
// Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'.
// Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox
class Box {
  content: string = "";
  sameAs(other: this) {
    return other.content === this.content;
  }
}
 
class DerivedBox extends Box {
  otherContent: string = "?";
}
 
const base = new Box();
const derived = new DerivedBox();
derived.sameAs(base); // base 报错
// Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'.
// Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox

this 基于 type guards:

您可以在类和接口中方法的返回位置使用此 is Type。当与类型收缩(例如 if 语句)混合时,目标对象的类型将被缩小到指定的 Type。

typescript
class FileSystemObject {
  isFile(): this is FileRep {
    return this instanceof FileRep;
  }
  isDirectory(): this is Directory {
    return this instanceof Directory;
  }
  isNetworked(): this is Networked & this {
    return this.networked;
  }
  constructor(public path: string, private networked: boolean) {}
}
 
class FileRep extends FileSystemObject {
  constructor(path: string, public content: string) {
    super(path, false);
  }
}
 
class Directory extends FileSystemObject {
  children: FileSystemObject[];
}
 
interface Networked {
  host: string;
}
 
const fso: FileSystemObject = new FileRep("foo/bar.txt", "foo");
 
if (fso.isFile()) {
  fso.content; // const fso: FileRep
} else if (fso.isDirectory()) {
  fso.children;// const fso: Directory
} else if (fso.isNetworked()) {
  fso.host;    // const fso: Networked & FileSystemObject
}
class FileSystemObject {
  isFile(): this is FileRep {
    return this instanceof FileRep;
  }
  isDirectory(): this is Directory {
    return this instanceof Directory;
  }
  isNetworked(): this is Networked & this {
    return this.networked;
  }
  constructor(public path: string, private networked: boolean) {}
}
 
class FileRep extends FileSystemObject {
  constructor(path: string, public content: string) {
    super(path, false);
  }
}
 
class Directory extends FileSystemObject {
  children: FileSystemObject[];
}
 
interface Networked {
  host: string;
}
 
const fso: FileSystemObject = new FileRep("foo/bar.txt", "foo");
 
if (fso.isFile()) {
  fso.content; // const fso: FileRep
} else if (fso.isDirectory()) {
  fso.children;// const fso: Directory
} else if (fso.isNetworked()) {
  fso.host;    // const fso: Networked & FileSystemObject
}

基于 this 的类型守卫的一个常见用例是允许对特定字段进行惰性验证。例如,当 hasValue 被验证为 true 时,这种情况会从 box 内保存的值中删除一个 undefined:

typescript
class Box<T> {
  value?: T;
 
  hasValue(): this is { value: T } {
    return this.value !== undefined;
  }
}
 
const box = new Box<string>();
box.value = "Gameboy";
box.value; // (property) Box<string>.value?: string
if (box.hasValue()) {
  box.value; // (property) value: string
}
class Box<T> {
  value?: T;
 
  hasValue(): this is { value: T } {
    return this.value !== undefined;
  }
}
 
const box = new Box<string>();
box.value = "Gameboy";
box.value; // (property) Box<string>.value?: string
if (box.hasValue()) {
  box.value; // (property) value: string
}

参数属性

TypeScript 提供了特殊的语法,用于将构造函数参数转换为具有相同名称和值的类属性。这些属性称为参数属性,通过在构造函数参数前加上可见性修饰符 publicprivateprotectedreadonly 之一来创建。结果字段将获得这些修饰符:

typescript
class Params {
  constructor(
    public readonly x: number,
    protected y: number,
    private z: number
  ) {
    // No body necessary
  }
}
const a = new Params(1, 2, 3);
console.log(a.x); // ok:(property) Params.x: number
console.log(a.z); // 报错:Property 'z' is private and only accessible within class 'Params'.
class Params {
  constructor(
    public readonly x: number,
    protected y: number,
    private z: number
  ) {
    // No body necessary
  }
}
const a = new Params(1, 2, 3);
console.log(a.x); // ok:(property) Params.x: number
console.log(a.z); // 报错:Property 'z' is private and only accessible within class 'Params'.

类表达式

类表达式与类声明非常相似。唯一真正的区别是类表达式不需要名称,尽管我们可以通过它们最终绑定到的任何标识符来引用它们:

typescript
const someClass = class<Type> {
  content: Type;
  constructor(value: Type) {
    this.content = value;
  }
};
const m = new someClass("Hello, world"); // const m: someClass<string>
const someClass = class<Type> {
  content: Type;
  constructor(value: Type) {
    this.content = value;
  }
};
const m = new someClass("Hello, world"); // const m: someClass<string>

构造函数签名

JavaScript 类使用 new 运算符进行实例化。给定类本身的类型,InstanceType 实用程序类型将对此作进行建模 -- InstanceType<typeof Point>

typescript
class Point {
  createdAt: number;
  x: number;
  y: number
  constructor(x: number, y: number) {
    this.createdAt = Date.now()
    this.x = x;
    this.y = y;
  }
}
type PointInstance = InstanceType<typeof Point>
 
function moveRight(point: PointInstance) {
  point.x += 5;
}
 
const point = new Point(3, 4);
moveRight(point);
point.x; // => 8
class Point {
  createdAt: number;
  x: number;
  y: number
  constructor(x: number, y: number) {
    this.createdAt = Date.now()
    this.x = x;
    this.y = y;
  }
}
type PointInstance = InstanceType<typeof Point>
 
function moveRight(point: PointInstance) {
  point.x += 5;
}
 
const point = new Point(3, 4);
moveRight(point);
point.x; // => 8

abstract 类和成员

TypeScript 中的类、方法和字段可以是抽象的。

抽象方法或抽象字段是尚未提供实现的方法。这些成员必须存在于抽象类中,而抽象类不能直接实例化。

抽象类的作用是充当实现所有抽象成员的子类的基类。当一个类没有任何抽象成员时,它被称为 concrete。

让我们看一个例子:

typescript
abstract class Base {
  abstract getName(): string;
  printName() {
    console.log("Hello, " + this.getName());
  }
}
const b = new Base(); //报错: 无法创建抽象类的实例。
abstract class Base {
  abstract getName(): string;
  printName() {
    console.log("Hello, " + this.getName());
  }
}
const b = new Base(); //报错: 无法创建抽象类的实例。

我们不能用 new 实例化 Base,因为它是抽象的。相反,我们需要创建一个派生类并实现抽象成员:

typescript
class Derived extends Base {
  getName() {
    return "world";
  }
}
const d = new Derived();
d.printName();
class Derived extends Base {
  getName() {
    return "world";
  }
}
const d = new Derived();
d.printName();

请注意,如果我们忘记实现基类的抽象成员,我们将收到一个错误:

typescript
class Derived extends Base {
  // 报错:非抽象类 'Derived' 不实现从类 'Base' 继承的抽象成员 getName。
}
class Derived extends Base {
  // 报错:非抽象类 'Derived' 不实现从类 'Base' 继承的抽象成员 getName。
}

抽象构造签名:

有时你想接受一些类构造函数,它产生一个从某个抽象类派生的类的实例。

例如,您可能希望编写以下代码:

typescript
function greet(ctor: typeof Base) {  
  const instance = new ctor();
  instance.printName();
}
// Cannot create an instance of an abstract class.

// Bad!
greet(Base);
function greet(ctor: typeof Base) {  
  const instance = new ctor();
  instance.printName();
}
// Cannot create an instance of an abstract class.

// Bad!
greet(Base);

TypeScript 正确地告诉你,你正在尝试实例化一个抽象类。毕竟,考虑到 greet 的定义,编写此代码是完全合法的,这最终会构造一个抽象类:

相反,您希望编写一个函数来接受具有结构签名的内容:

typescript
function greet(ctor: new () => Base) {
  const instance = new ctor();
  instance.printName();
}
greet(Derived);
greet(Base); // 报错:Base
// Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'.
// Cannot assign an abstract constructor type to a non-abstract constructor type.
function greet(ctor: new () => Base) {
  const instance = new ctor();
  instance.printName();
}
greet(Derived);
greet(Base); // 报错:Base
// Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'.
// Cannot assign an abstract constructor type to a non-abstract constructor type.

现在 TypeScript 可以正确地告诉你哪些类构造函数可以被调用 - Derived 可以,因为它是具体的,但 Base 不能。

类与类之间的关系

在大多数情况下,TypeScript 中的类在结构上进行比较,与其他类型的类相同。

例如,这两个类可以相互替代,因为它们是相同的:

typescript
class Point1 {
  x = 0;
  y = 0;
}
 
class Point2 {
  x = 0;
  y = 0;
}
 
// OK
const p: Point1 = new Point2();
class Point1 {
  x = 0;
  y = 0;
}
 
class Point2 {
  x = 0;
  y = 0;
}
 
// OK
const p: Point1 = new Point2();

同样,即使没有显式继承,类之间也存在子类型关系:

typescript
class Person {
  name: string;
  age: number;
}
 
class Employee {
  name: string;
  age: number;
  salary: number;
}
 
// OK
const p: Person = new Employee();
class Person {
  name: string;
  age: number;
}
 
class Employee {
  name: string;
  age: number;
  salary: number;
}
 
// OK
const p: Person = new Employee();

这听起来很简单,但有一些情况似乎比其他情况更奇怪。

空类没有成员。在结构类型系统中,没有成员的类型通常是其他任何类型的超类型。因此,如果你写了一个空的类(不要!),可以使用任何东西来代替它:

typescript
class Empty {}
 
function fn(x: Empty) {
  // can't do anything with 'x', so I won't
}
 
// All OK!
fn(window);
fn({});
fn(fn);
class Empty {}
 
function fn(x: Empty) {
  // can't do anything with 'x', so I won't
}
 
// All OK!
fn(window);
fn({});
fn(fn);