背景阅读
类 (MDN)

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

与其他 JavaScript 语言特性一样,TypeScript 添加了类型注解和其他语法,允许你表达类与其他类型之间的关系。

类成员

这是最基本的类——一个空类

ts
class Point {}
Try

这个类目前还没什么用,所以让我们开始添加一些成员。

字段

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

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

与其他地方一样,类型注解是可选的;如果未指定,将隐式推断为 any

字段也可以有初始化程序;这些代码将在类实例化时自动运行

ts
class Point {
x = 0;
y = 0;
}
 
const pt = new Point();
// Prints 0, 0
console.log(`${pt.x}, ${pt.y}`);
Try

就像 constletvar 一样,类属性的初始化程序将用于推断其类型

ts
const pt = new Point();
pt.x = "0";
Type 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.
Try

--strictPropertyInitialization

strictPropertyInitialization 设置控制类字段是否必须在构造函数中进行初始化。

ts
class BadGreeter {
name: string;
Property 'name' has no initializer and is not definitely assigned in the constructor.2564Property 'name' has no initializer and is not definitely assigned in the constructor.
}
Try
ts
class GoodGreeter {
name: string;
 
constructor() {
this.name = "hello";
}
}
Try

请注意,字段需要在构造函数本身中进行初始化。TypeScript 不会分析你在构造函数中调用的方法来检测初始化,因为派生类可能会重写这些方法,从而导致成员未能初始化。

如果你打算通过构造函数以外的方式来确切初始化字段(例如,也许某个外部库在为你填充部分类),你可以使用明确赋值断言操作符 !

ts
class OKGreeter {
// Not initialized, but no error
name!: string;
}
Try

readonly

字段可以使用 readonly 修饰符作为前缀。这可以防止在构造函数之外对该字段进行赋值。

ts
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.2540Cannot 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.2540Cannot assign to 'name' because it is a read-only property.
Try

构造函数

背景阅读
构造函数 (MDN)

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

ts
class Point {
x: number;
y: number;
 
// Normal signature with defaults
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
Try
ts
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
}
}
Try

类构造函数签名与函数签名之间只有几点差异:

  • 构造函数不能有类型参数——这些属于外部类声明,我们稍后会学习。
  • 构造函数不能有返回类型注解——返回的始终是类实例类型。

Super 调用

正如在 JavaScript 中一样,如果你有基类,则需要在构造函数体内使用任何 this. 成员之前调用 super();

ts
class Base {
k = 4;
}
 
class Derived extends Base {
constructor() {
// Prints a wrong value in ES5; throws exception in ES6
console.log(this.k);
'super' must be called before accessing 'this' in the constructor of a derived class.17009'super' must be called before accessing 'this' in the constructor of a derived class.
super();
}
}
Try

忘记调用 super 是 JavaScript 中很容易犯的错误,但 TypeScript 会在必要时提醒你。

方法

背景阅读
方法定义

类上的函数属性称为方法。方法可以使用与函数和构造函数完全相同的类型注解。

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

除了标准的类型注解外,TypeScript 不会为方法添加任何其他新内容。

请注意,在方法体内,仍然必须通过 this. 访问字段和其他方法。方法体内未限定的名称始终指向封闭作用域中的内容。

ts
let x: number = 0;
 
class C {
x: string = "hello";
 
m() {
// This is trying to modify 'x' from line 1, not the class property
x = "world";
Type 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.
}
}
Try

Getter / Setter

类也可以有存取器

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

请注意,在 JavaScript 中,没有额外逻辑的字段支持 get/set 对极少有用。如果你不需要在 get/set 操作期间添加额外逻辑,直接暴露公共字段完全没问题。

TypeScript 对存取器有一些特殊的推断规则:

  • 如果存在 get 但没有 set,则属性自动为 readonly
  • 如果未指定 setter 参数的类型,则会从 getter 的返回类型中推断得出。

TypeScript 4.3 起,可以为 getter 和 setter 设置不同的类型。

ts
class Thing {
_size = 0;
 
get size(): number {
return this._size;
}
 
set size(value: string | number | boolean) {
let num = Number(value);
 
// Don't allow NaN, Infinity, etc
 
if (!Number.isFinite(num)) {
this._size = 0;
return;
}
 
this._size = num;
}
}
Try

索引签名

类可以声明索引签名;它们的工作方式与 其他对象类型的索引签名 相同。

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

因为索引签名类型还需要捕获方法的类型,所以很难有效地使用这些类型。通常最好将索引数据存储在其他地方,而不是直接存储在类实例上。

类继承

像其他具有面向对象特性的语言一样,JavaScript 中的类可以从基类继承。

implements 子句

你可以使用 implements 子句来检查类是否满足特定的 interface。如果类未能正确实现它,将会报错。

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

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

注意事项

重要的是要明白,implements 子句仅用于检查该类是否可以被当作接口类型来处理。它根本不会改变类或其方法的类型。一个常见的错误是假设 implements 子句会改变类类型——其实不会!

ts
interface Checkable {
check(name: string): boolean;
}
 
class NameChecker implements Checkable {
check(s) {
Parameter 's' implicitly has an 'any' type.7006Parameter 's' implicitly has an 'any' type.
// Notice no error here
return s.toLowerCase() === "ok";
any
}
}
Try

在这个例子中,我们可能期望 s 的类型会受到 checkname: string 参数的影响。但事实并非如此——implements 子句不会改变类主体的检查方式或其类型推断方式。

同样,实现一个带有可选属性的接口并不会创建该属性。

ts
interface A {
x: number;
y?: number;
}
class C implements A {
x = 0;
}
const c = new C();
c.y = 10;
Property 'y' does not exist on type 'C'.2339Property 'y' does not exist on type 'C'.
Try

extends 子句

背景阅读
extends 关键字 (MDN)

类可以 extend(继承)自基类。派生类拥有其基类的所有属性和方法,还可以定义额外的成员。

ts
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();
// Base class method
d.move();
// Derived class method
d.woof(3);
Try

重写方法

背景阅读
super 关键字 (MDN)

派生类也可以重写基类的字段或属性。你可以使用 super. 语法来访问基类方法。注意,由于 JavaScript 类是一个简单的查找对象,因此没有“super 字段”的概念。

TypeScript 强制要求派生类始终是其基类的子类型。

例如,以下是一种重写方法的合法方式

ts
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()}`);
}
}
}
 
const d = new Derived();
d.greet();
d.greet("reader");
Try

重要的是,派生类必须遵循其基类的契约。请记住,通过基类引用来引用派生类实例是非常常见的(且总是合法的!)

ts
// Alias the derived instance through a base class reference
const b: Base = d;
// No problem
b.greet();
Try

如果 Derived 不遵循 Base 的契约会怎样?

ts
class Base {
greet() {
console.log("Hello, world!");
}
}
 
class Derived extends Base {
// Make this parameter required
greet(name: string) {
Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'. Type '(name: string) => void' is not assignable to type '() => void'. Target signature provides too few arguments. Expected 1 or more, but got 0.2416Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'. Type '(name: string) => void' is not assignable to type '() => void'. Target signature provides too few arguments. Expected 1 or more, but got 0.
console.log(`Hello, ${name.toUpperCase()}`);
}
}
Try

如果我们忽略错误编译此代码,该示例最终会崩溃。

ts
const b: Base = new Derived();
// Crashes because "name" will be undefined
b.greet();
Try

仅类型字段声明

target >= ES2022useDefineForClassFieldstrue 时,类字段会在父类构造函数完成后进行初始化,覆盖父类设置的任何值。当你只想为继承的字段重新声明更精确的类型时,这可能会成为问题。为了处理这些情况,你可以编写 declare 来向 TypeScript 指示此字段声明不应产生运行时效果。

ts
interface Animal {
dateOfBirth: any;
}
 
interface Dog extends Animal {
breed: any;
}
 
class AnimalHouse {
resident: Animal;
constructor(animal: Animal) {
this.resident = animal;
}
}
 
class DogHouse extends AnimalHouse {
// Does not emit JavaScript code,
// only ensures the types are correct
declare resident: Dog;
constructor(dog: Dog) {
super(dog);
}
}
Try

初始化顺序

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

ts
class Base {
name = "base";
constructor() {
console.log("My name is " + this.name);
}
}
 
class Derived extends Base {
name = "derived";
}
 
// Prints "base", not "derived"
const d = new Derived();
Try

发生了什么?

JavaScript 定义的类初始化顺序如下:

  • 基类字段被初始化
  • 基类构造函数运行
  • 派生类字段被初始化
  • 派生类构造函数运行

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

继承内置类型

注意:如果你不打算继承内置类型(如 ArrayErrorMap 等),或者你的编译目标明确设置为 ES6/ES2015 或更高版本,你可以跳过此部分。

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

因此,对 ErrorArray 等进行子类化可能无法按预期工作。这是因为 ErrorArray 等的构造函数使用 ECMAScript 6 的 new.target 来调整原型链;然而,在 ECMAScript 5 中调用构造函数时,无法确保 new.target 的值。其他向下兼容的编译器默认通常也有同样的限制。

对于如下的子类:

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

你可能会发现:

  • 在构造这些子类返回的对象上,方法可能是 undefined,因此调用 sayHello 会导致错误。
  • 子类实例与它们实例之间的 instanceof 将失效,因此 (new MsgError()) instanceof MsgError 将返回 false

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

ts
class MsgError extends Error {
constructor(m: string) {
super(m);
 
// Set the prototype explicitly.
Object.setPrototypeOf(this, MsgError.prototype);
}
 
sayHello() {
return "hello " + this.message;
}
}
Try

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

不幸的是,这些变通方法在 Internet Explorer 10 及更早版本上不起作用。你可以手动将原型上的方法复制到实例本身上(即 MsgError.prototypethis 上),但原型链本身无法修复。

成员可见性

你可以使用 TypeScript 控制某些方法或属性对类外部代码是否可见。

public

类成员的默认可见性是 publicpublic 成员可以在任何地方被访问。

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

因为 public 已经是默认的可见性修饰符,你不需要在类成员上显式编写它,但出于风格/可读性的考虑,你也可以选择添加。

protected

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

ts
class Greeter {
public greet() {
console.log("Hello, " + this.getName());
}
protected getName() {
return "hi";
}
}
 
class SpecialGreeter extends Greeter {
public howdy() {
// OK to access protected member here
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.2445Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.
Try

暴露 protected 成员

派生类需要遵循其基类的契约,但可以选择暴露具有更多功能的基类子类型。这包括将 protected 成员变为 public

ts
class Base {
protected m = 10;
}
class Derived extends Base {
// No modifier, so default is 'public'
m = 15;
}
const d = new Derived();
console.log(d.m); // OK
Try

请注意,Derived 本来就已经能够自由读取和写入 m,所以这并不会从根本上改变这种情况的“安全性”。这里主要要注意的是,在派生类中,如果这种暴露不是故意的,我们需要小心重复使用 protected 修饰符。

跨层级 protected 访问

TypeScript 不允许在类层级结构中访问兄弟类的 protected 成员。

ts
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.2445Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.
}
}
Try

这是因为在 Derived2 中访问 x 应该仅对 Derived2 的子类合法,而 Derived1 并不是其中之一。此外,如果通过 Derived1 引用访问 x 是非法的(这理所应当是非法的!),那么通过基类引用访问它也绝不应该改善这种情况。

另请参阅 为什么我不能从派生类访问受保护的成员?,文中对 C# 中关于相同主题的推理进行了更多解释。

private

privateprotected 类似,但即便是子类也不允许访问该成员。

ts
class Base {
private x = 0;
}
const b = new Base();
// Can't access from outside the class
console.log(b.x);
Property 'x' is private and only accessible within class 'Base'.2341Property 'x' is private and only accessible within class 'Base'.
Try
ts
class Derived extends Base {
showX() {
// Can't access in subclasses
console.log(this.x);
Property 'x' is private and only accessible within class 'Base'.2341Property 'x' is private and only accessible within class 'Base'.
}
}
Try

因为 private 成员对派生类不可见,所以派生类无法提高它们的可见性。

ts
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'.2415Class 'Derived' incorrectly extends base class 'Base'. Property 'x' is private in type 'Base' but not in type 'Derived'.
x = 1;
}
Try

跨实例 private 访问

不同的面向对象编程语言对于“同一类的不同实例是否可以访问彼此的 private 成员”意见不一。虽然 Java、C#、C++、Swift 和 PHP 等语言允许这样做,但 Ruby 不允许。

TypeScript 确实允许跨实例的 private 访问。

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

注意事项

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

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

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

private 在类型检查期间也允许使用括号表示法进行访问。这使得 private 声明的字段在单元测试等场景下可能更容易访问,缺点是这些字段只是软私有(soft private),并不严格强制执行私有性。

ts
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'.2341Property 'secretKey' is private and only accessible within class 'MySafe'.
 
// OK
console.log(s["secretKey"]);
Try

与 TypeScript 的 private 不同,JavaScript 的 私有字段 (#) 在编译后仍然保持私有,并且不提供像括号表示法访问那样的逃生舱,使它们成为硬私有(hard private)。

ts
class Dog {
#barkAmount = 0;
personality = "happy";
 
constructor() {}
}
Try
ts
"use strict";
class Dog {
#barkAmount = 0;
personality = "happy";
constructor() { }
}
 
Try

当编译为 ES2021 或更低版本时,TypeScript 将使用 WeakMap 来代替 #

ts
"use strict";
var _Dog_barkAmount;
class Dog {
constructor() {
_Dog_barkAmount.set(this, 0);
this.personality = "happy";
}
}
_Dog_barkAmount = new WeakMap();
 
Try

如果你需要保护类中的值免受恶意行为者的侵害,你应该使用提供硬运行时私有性的机制,如闭包、WeakMap 或私有字段。请注意,这些在运行时添加的隐私检查可能会影响性能。

静态成员

背景阅读
静态成员 (MDN)

类可以有 static(静态)成员。这些成员不与类的特定实例关联。它们可以通过类构造函数对象本身来访问。

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

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

ts
class MyClass {
private static x = 0;
}
console.log(MyClass.x);
Property 'x' is private and only accessible within class 'MyClass'.2341Property 'x' is private and only accessible within class 'MyClass'.
Try

静态成员也是可以继承的。

ts
class Base {
static getGreeting() {
return "Hello world";
}
}
class Derived extends Base {
myGreeting = Derived.getGreeting();
}
Try

特殊的静态名称

重写 Function 原型上的属性通常是不安全/不可行的。因为类本身就是可以用 new 调用的函数,所以某些 static 名称不能使用。像 namelengthcall 这样的函数属性不能定义为 static 成员。

ts
class S {
static name = "S!";
Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.2699Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.
}
Try

为什么没有静态类?

TypeScript(和 JavaScript)没有像 C# 那样名为 static class 的构造。

这些构造之所以存在,仅仅是因为那些语言强制所有数据和函数都必须在类内部;而由于 TypeScript 中不存在这种限制,因此不需要它们。仅有一个实例的类通常在 JavaScript/TypeScript 中仅表示为一个普通对象

例如,我们不需要 TypeScript 中的“静态类”语法,因为普通对象(甚至顶层函数)完全可以胜任这项工作。

ts
// Unnecessary "static" class
class MyStaticClass {
static doSomething() {}
}
 
// Preferred (alternative 1)
function doSomething() {}
 
// Preferred (alternative 2)
const MyHelperObject = {
dosomething() {},
};
Try

类中的 static

静态块允许你编写一系列具有自身作用域的语句,这些语句可以访问包含类中的私有字段。这意味着我们可以编写具有所有语句编写能力的初始化代码,不会造成变量泄露,并且可以完全访问类的内部结构。

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

泛型类

类和接口一样,可以是泛型的。当泛型类使用 new 实例化时,其类型参数的推断方式与函数调用相同。

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

类可以使用与接口相同的泛型约束和默认值。

静态成员中的类型参数

这段代码是不合法的,其原因可能并不显而易见。

ts
class Box<Type> {
static defaultValue: Type;
Static members cannot reference class type parameters.2302Static members cannot reference class type parameters.
}
Try

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

类中运行时的 this

背景阅读
this 关键字 (MDN)

重要的是要记住,TypeScript 不会改变 JavaScript 的运行时行为,而 JavaScript 以具有某些独特的运行时行为而闻名。

JavaScript 对 this 的处理确实非常特殊。

ts
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());
Try

简而言之,默认情况下,函数内部 this 的值取决于函数是如何被调用的。在此示例中,因为函数是通过 obj 引用调用的,所以其 this 的值是 obj,而不是类实例。

这很少是你想要的结果!TypeScript 提供了一些方法来减轻或防止这类错误。

箭头函数

背景阅读
箭头函数 (MDN)

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

ts
class MyClass {
name = "MyClass";
getName = () => {
return this.name;
};
}
const c = new MyClass();
const g = c.getName;
// Prints "MyClass" instead of crashing
console.log(g());
Try

这有一些权衡:

  • 即使对于未经过 TypeScript 检查的代码,this 值在运行时也保证是正确的。
  • 这会消耗更多内存,因为每个类实例都会拥有以这种方式定义的每个函数的副本。
  • 你不能在派生类中使用 super.getName,因为原型链中没有条目可以从中获取基类方法。

this 参数

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

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

TypeScript 会检查调用带有 this 参数的函数时是否具有正确的上下文。与其使用箭头函数,我们可以在方法定义中添加一个 this 参数,以静态强制该方法被正确调用。

ts
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'.2684The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.
Try

这种方法与箭头函数方法存在相反的权衡:

  • JavaScript 调用者可能仍然会不经意地错误使用类方法。
  • 每个类定义只会分配一个函数,而不是每个类实例分配一个。
  • 基方法定义仍然可以通过 super 调用。

this 类型

在类中,一种名为 this 的特殊类型会动态引用当前类的类型。让我们看看这有何用处。

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

在这里,TypeScript 将 set 的返回类型推断为 this,而不是 Box。现在让我们创建一个 Box 的子类。

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

你也可以在参数类型注解中使用 this

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

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

ts
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);
Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.2345Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.
Try

基于 this 的类型保护

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

ts
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
}
Try

基于 this 的类型保护的一个常见用例是允许对特定字段进行懒验证。例如,当 hasValue 被验证为 true 时,此用例会从 box 内持有的值中移除一个 undefined

ts
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
}
Try

参数属性

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

ts
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);
(property) Params.x: number
console.log(a.z);
Property 'z' is private and only accessible within class 'Params'.2341Property 'z' is private and only accessible within class 'Params'.
Try

类表达式

背景阅读
类表达式 (MDN)

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

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

构造函数签名

JavaScript 类是用 new 操作符实例化的。给定类本身的类型,InstanceType 工具类型可以模拟此操作。

ts
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
Try

abstract 类和成员

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

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

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

让我们看一个例子。

ts
abstract class Base {
abstract getName(): string;
 
printName() {
console.log("Hello, " + this.getName());
}
}
 
const b = new Base();
Cannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.
Try

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

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

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

ts
class Derived extends Base {
Non-abstract class 'Derived' does not implement inherited abstract member getName from class 'Base'.2515Non-abstract class 'Derived' does not implement inherited abstract member getName from class 'Base'.
// forgot to do anything
}
Try

抽象构造签名

有时你想要接受某种产生从特定抽象类派生的实例的类构造函数。

例如,你可能想要编写这段代码。

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

TypeScript 正确地告诉你,你正试图实例化一个抽象类。毕竟,鉴于 greet 的定义,编写此代码是完全合法的,但这最终会构建一个抽象类。

ts
// Bad!
greet(Base);
Try

相反,你需要编写一个接受具有构造签名的内容的函数。

ts
function greet(ctor: new () => Base) {
const instance = new ctor();
instance.printName();
}
greet(Derived);
greet(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.2345Argument 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.
Try

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

类之间的关系

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

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

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

类似地,即使没有显式继承,类之间也存在子类型关系。

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

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

空类没有成员。在结构化类型系统中,没有成员的类型通常是任何其他类型的超类型。因此,如果你编写一个空类(别这么做!),任何内容都可以用来替换它。

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

TypeScript 文档是一个开源项目。请通过 提交 Pull Request 来帮助我们改进这些页面 ❤

此页面的贡献者
RCRyan Cavanaugh (60)
OTOrta Therox (15)
HAHossein Ahmadian-Yazdi (6)
MRMaxim R (3)
Uuid11 (2)
23+

最后更新:2026 年 3 月 27 日