背景阅读:
类(MDN)

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

¥TypeScript offers full support for the class keyword introduced in ES2015.

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

¥As with other JavaScript language features, TypeScript adds type annotations and other syntax to allow you to express relationships between classes and other types.

类成员

¥Class Members

这是最基本的类 - 一个空的:

¥Here’s the most basic class - an empty one:

ts
class Point {}
Try

这个类还不是很有用,所以让我们开始添加一些成员。

¥This class isn’t very useful yet, so let’s start adding some members.

字段

¥Fields

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

¥A field declaration creates a public writeable property on a class:

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

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

¥As with other locations, the type annotation is optional, but will be an implicit any if not specified.

字段也可以有初始化器;这些将在实例化类时自动运行:

¥Fields can also have initializers; these will run automatically when the class is instantiated:

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

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

¥Just like with const, let, and var, the initializer of a class property will be used to infer its type:

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 设置控制类字段是否需要在构造函数中初始化。

¥The strictPropertyInitialization setting controls whether class fields need to be initialized in the constructor.

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 不会分析你从构造函数调用的方法来检测初始化,因为派生类可能会覆盖这些方法并且无法初始化成员。

¥Note that the field needs to be initialized in the constructor itself. TypeScript does not analyze methods you invoke from the constructor to detect initializations, because a derived class might override those methods and fail to initialize the members.

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

¥If you intend to definitely initialize a field through means other than the constructor (for example, maybe an external library is filling in part of your class for you), you can use the definite assignment assertion operator, !:

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

readonly

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

¥Fields may be prefixed with the readonly modifier. This prevents assignments to the field outside of the constructor.

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

构造器

¥Constructors

背景阅读:
构造函数(MDN)

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

¥Class constructors are very similar to functions. You can add parameters with type annotations, default values, and overloads:

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

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

¥There are just a few differences between class constructor signatures and function signatures:

  • 构造函数不能有类型参数 - 这些属于外部类声明,我们稍后会了解

    ¥Constructors can’t have type parameters - these belong on the outer class declaration, which we’ll learn about later

  • 构造函数不能有返回类型注释 - 类实例类型始终是返回的内容

    ¥Constructors can’t have return type annotations - the class instance type is always what’s returned

超类调用

¥Super Calls

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

¥Just as in JavaScript, if you have a base class, you’ll need to call super(); in your constructor body before using any this. members:

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

在 JavaScript 中忘记调用 super 是一个容易犯的错误,但 TypeScript 会在必要时告诉你。

¥Forgetting to call super is an easy mistake to make in JavaScript, but TypeScript will tell you when it’s necessary.

方法

¥Methods

背景阅读:
方法定义

类上的函数属性称为方法。方法可以使用所有与函数和构造函数相同的类型注释:

¥A function property on a class is called a method. Methods can use all the same type annotations as functions and constructors:

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

除了标准的类型注解,TypeScript 没有为方法添加任何新的东西。

¥Other than the standard type annotations, TypeScript doesn’t add anything else new to methods.

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

¥Note that inside a method body, it is still mandatory to access fields and other methods via this.. An unqualified name in a method body will always refer to something in the enclosing scope:

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

获取器/设置器

¥Getters / Setters

类也可以有访问器:

¥Classes can also have accessors:

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

请注意,没有额外逻辑的由字段支持的 get/set 对在 JavaScript 中很少有用。如果你不需要在 get/set 操作期间添加其他逻辑,则可以公开公共字段。

¥Note that a field-backed get/set pair with no extra logic is very rarely useful in JavaScript. It’s fine to expose public fields if you don’t need to add additional logic during the get/set operations.

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

¥TypeScript has some special inference rules for accessors:

  • 如果 get 存在但没有 set,则属性自动为 readonly

    ¥If get exists but no set, the property is automatically readonly

  • 如果不指定 setter 参数的类型,则从 getter 的返回类型推断

    ¥If the type of the setter parameter is not specified, it is inferred from the return type of the getter

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

¥Since TypeScript 4.3, it is possible to have accessors with different types for getting and setting.

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

索引签名

¥Index Signatures

类可以声明索引签名;这些工作与 其他对象类型的索引签名 相同:

¥Classes can declare index signatures; these work the same as Index Signatures for other object types:

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

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

¥Because the index signature type needs to also capture the types of methods, it’s not easy to usefully use these types. Generally it’s better to store indexed data in another place instead of on the class instance itself.

类继承

¥Class Heritage

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

¥Like other languages with object-oriented features, classes in JavaScript can inherit from base classes.

implements 从句

¥implements Clauses

你可以使用 implements 子句来检查一个类是否满足特定的 interface。如果一个类未能正确实现它,则会触发错误:

¥You can use an implements clause to check that a class satisfies a particular interface. An error will be issued if a class fails to correctly implement it:

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 {

¥Classes may also implement multiple interfaces, e.g. class C implements A, B {.

注意事项

¥Cautions

重要的是要理解 implements 子句只是检查类可以被视为接口类型。它根本不会改变类的类型或其方法。常见的错误来源是假设 implements 子句将更改类类型 - 事实并非如此!

¥It’s important to understand that an implements clause is only a check that the class can be treated as the interface type. It doesn’t change the type of the class or its methods at all. A common source of error is to assume that an implements clause will change the class type - it doesn’t!

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 子句不会更改类主体的检查方式或其类型推断方式。

¥In this example, we perhaps expected that s’s type would be influenced by the name: string parameter of check. It is not - implements clauses don’t change how the class body is checked or its type inferred.

同样,使用可选属性实现接口不会创建该属性:

¥Similarly, implementing an interface with an optional property doesn’t create that property:

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 Clauses

背景阅读:
扩展关键字 (MDN)

类可能来自基类。派生类具有其基类的所有属性和方法,还可以定义额外的成员。

¥Classes may extend from a base class. A derived class has all the properties and methods of its base class, and can also define additional members.

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

覆盖方法

¥Overriding Methods

背景阅读:
超级关键字(MDN)

派生类也可以覆盖基类字段或属性。你可以使用 super. 语法来访问基类方法。请注意,因为 JavaScript 类是一个简单的查找对象,所以没有 “超级字段” 的概念。

¥A derived class can also override a base class field or property. You can use the super. syntax to access base class methods. Note that because JavaScript classes are a simple lookup object, there is no notion of a “super field”.

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

¥TypeScript enforces that a derived class is always a subtype of its base class.

例如,这是覆盖方法的合法方式:

¥For example, here’s a legal way to override a method:

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

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

¥It’s important that a derived class follow its base class contract. Remember that it’s very common (and always legal!) to refer to a derived class instance through a base class reference:

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

如果 Derived 不遵守 Base 的合同怎么办?

¥What if Derived didn’t follow Base’s contract?

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

如果我们在出现错误的情况下编译此代码,则此示例将崩溃:

¥If we compiled this code despite the error, this sample would then crash:

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

仅类型字段声明

¥Type-only Field Declarations

target >= ES2022useDefineForClassFieldstrue 时,在父类构造函数完成后初始化类字段,覆盖父类设置的任何值。当你只想为继承的字段重新声明更准确的类型时,这可能会成为问题。为了处理这些情况,你可以写 declare 来向 TypeScript 表明这个字段声明不应该有运行时影响。

¥When target >= ES2022 or useDefineForClassFields is true, class fields are initialized after the parent class constructor completes, overwriting any value set by the parent class. This can be a problem when you only want to re-declare a more accurate type for an inherited field. To handle these cases, you can write declare to indicate to TypeScript that there should be no runtime effect for this field declaration.

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

初始化顺序

¥Initialization Order

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

¥The order that JavaScript classes initialize can be surprising in some cases. Let’s consider this code:

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

这里发生了什么?

¥What happened here?

JavaScript 定义的类初始化顺序是:

¥The order of class initialization, as defined by JavaScript, is:

  • 基类字段被初始化

    ¥The base class fields are initialized

  • 基类构造函数运行

    ¥The base class constructor runs

  • 派生类字段被初始化

    ¥The derived class fields are initialized

  • 派生类构造函数运行

    ¥The derived class constructor runs

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

¥This means that the base class constructor saw its own value for name during its own constructor, because the derived class field initializations hadn’t run yet.

继承内置类型

¥Inheriting Built-in Types

注意:如果你不打算继承 ArrayErrorMap 等内置类型,或者你的编译目标明确设置为 ES6/ES2015 或以上,则可以跳过本节

¥Note: If you don’t plan to inherit from built-in types like Array, Error, Map, etc. or your compilation target is explicitly set to ES6/ES2015 or above, you may skip this section

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

¥In ES2015, constructors which return an object implicitly substitute the value of this for any callers of super(...). It is necessary for generated constructor code to capture any potential return value of super(...) and replace it with this.

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

¥As a result, subclassing Error, Array, and others may no longer work as expected. This is due to the fact that constructor functions for Error, Array, and the like use ECMAScript 6’s new.target to adjust the prototype chain; however, there is no way to ensure a value for new.target when invoking a constructor in ECMAScript 5. Other downlevel compilers generally have the same limitation by default.

对于如下子类:

¥For a subclass like the following:

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

你可能会发现:

¥you may find that:

  • 方法可能是构造这些子类返回的对象上的 undefined,所以调用 sayHello 会导致错误。

    ¥methods may be undefined on objects returned by constructing these subclasses, so calling sayHello will result in an error.

  • instanceof 将在子类的实例及其实例之间断开,因此 (new MsgError()) instanceof MsgError 将返回 false

    ¥instanceof will be broken between instances of the subclass and their instances, so (new MsgError()) instanceof MsgError will return false.

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

¥As a recommendation, you can manually adjust the prototype immediately after any super(...) calls.

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__

¥However, any subclass of MsgError will have to manually set the prototype as well. For runtimes that don’t support Object.setPrototypeOf, you may instead be able to use __proto__.

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

¥Unfortunately, these workarounds will not work on Internet Explorer 10 and prior. One can manually copy methods from the prototype onto the instance itself (i.e. MsgError.prototype onto this), but the prototype chain itself cannot be fixed.

成员可见性

¥Member Visibility

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

¥You can use TypeScript to control whether certain methods or properties are visible to code outside the class.

public

类成员的默认可见性为 publicpublic 成员可以在任何地方访问:

¥The default visibility of class members is public. A public member can be accessed anywhere:

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

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

¥Because public is already the default visibility modifier, you don’t ever need to write it on a class member, but might choose to do so for style/readability reasons.

protected

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

¥protected members are only visible to subclasses of the class they’re declared in.

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 成员

¥Exposure of protected members

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

¥Derived classes need to follow their base class contracts, but may choose to expose a subtype of base class with more capabilities. This includes making protected members 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,因此这并没有有意义地改变这种情况下的 “security”。这里需要注意的主要一点是,在派生类中,如果这种暴露不是故意的,我们需要小心重复 protected 修饰符。

¥Note that Derived was already able to freely read and write m, so this doesn’t meaningfully alter the “security” of this situation. The main thing to note here is that in the derived class, we need to be careful to repeat the protected modifier if this exposure isn’t intentional.

跨层级 protected 访问

¥Cross-hierarchy protected access

不同的 OOP 语言对于通过基类引用访问 protected 成员是否合法存在分歧:

¥Different OOP languages disagree about whether it’s legal to access a protected member through a base class reference:

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

例如,Java 认为这是合法的。另一方面,C# 和 C++ 选择此代码应该是非法的。

¥Java, for example, considers this to be legal. On the other hand, C# and C++ chose that this code should be illegal.

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

¥TypeScript sides with C# and C++ here, because accessing x in Derived2 should only be legal from Derived2’s subclasses, and Derived1 isn’t one of them. Moreover, if accessing x through a Derived1 reference is illegal (which it certainly should be!), then accessing it through a base class reference should never improve the situation.

另请参阅 为什么我不能从派生类访问受保护的成员?,它解释了 C# 的更多推断。

¥See also Why Can’t I Access A Protected Member From A Derived Class? which explains more of C#‘s reasoning.

private

private 类似于 protected,但不允许从子类访问成员:

¥private is like protected, but doesn’t allow access to the member even from subclasses:

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 成员对派生类不可见,所以派生类不能增加它们的可见性:

¥Because private members aren’t visible to derived classes, a derived class can’t increase their visibility:

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 访问

¥Cross-instance private access

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

¥Different OOP languages disagree about whether different instances of the same class may access each others’ private members. While languages like Java, C#, C++, Swift, and PHP allow this, Ruby does not.

TypeScript 确实允许跨实例 private 访问:

¥TypeScript does allow cross-instance private access:

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

警告

¥Caveats

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

¥Like other aspects of TypeScript’s type system, private and protected are only enforced during type checking.

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

¥This means that JavaScript runtime constructs like in or simple property lookup can still access a private or protected member:

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 声明的字段可能更容易访问,例如单元测试,缺点是这些字段是软私有的并且不严格执行隐私。

¥private also allows access using bracket notation during type checking. This makes private-declared fields potentially easier to access for things like unit tests, with the drawback that these fields are soft private and don’t strictly enforce privacy.

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

与 TypeScripts 的 private 不同,JavaScript 的 私有字段 (#) 在编译后仍然是私有的,并且不提供前面提到的像括号符号访问这样的转义舱口,这使得它们很难私有。

¥Unlike TypeScripts’s private, JavaScript’s private fields (#) remain private after compilation and do not provide the previously mentioned escape hatches like bracket notation access, making them hard private.

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

当编译到 ES2021 或更低版本时,TypeScript 将使用 Wea​​kMaps 代替 #

¥When compiling to ES2021 or less, TypeScript will use WeakMaps in place of #.

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

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

¥If you need to protect values in your class from malicious actors, you should use mechanisms that offer hard runtime privacy, such as closures, WeakMaps, or private fields. Note that these added privacy checks during runtime could affect performance.

静态成员

¥Static Members

背景阅读:
静态成员 (MDN)

类可能有 static 个成员。这些成员不与类的特定实例相关联。它们可以通过类构造函数对象本身访问:

¥Classes may have static members. These members aren’t associated with a particular instance of the class. They can be accessed through the class constructor object itself:

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

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

¥Static members can also use the same public, protected, and private visibility modifiers:

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

静态成员也被继承:

¥Static members are also inherited:

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

特殊静态名称

¥Special Static Names

Function 原型覆盖属性通常是不安全/不可能的。因为类本身就是可以用 new 调用的函数,所以不能使用某些 static 名称。namelengthcall 等函数属性无法定义为 static 成员:

¥It’s generally not safe/possible to overwrite properties from the Function prototype. Because classes are themselves functions that can be invoked with new, certain static names can’t be used. Function properties like name, length, and call aren’t valid to define as static members:

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

为什么没有静态类?

¥Why No Static Classes?

TypeScript(和 JavaScript)没有一个名为 static class 的构造,就像 C# 一样。

¥TypeScript (and JavaScript) don’t have a construct called static class the same way as, for example, C# does.

这些构造之所以存在,是因为这些语言强制所有数据和函数都在一个类中;因为 TypeScript 中不存在该限制,所以不需要它们。只有一个实例的类通常只表示为 JavaScript/TypeScript 中的普通对象。

¥Those constructs only exist because those languages force all data and functions to be inside a class; because that restriction doesn’t exist in TypeScript, there’s no need for them. A class with only a single instance is typically just represented as a normal object in JavaScript/TypeScript.

例如,我们不需要 TypeScript 中的 “静态类” 语法,因为常规对象(甚至顶层函数)也可以完成这项工作:

¥For example, we don’t need a “static class” syntax in TypeScript because a regular object (or even top-level function) will do the job just as well:

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

static 类中的块

¥static Blocks in Classes

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

¥Static blocks allow you to write a sequence of statements with their own scope that can access private fields within the containing class. This means that we can write initialization code with all the capabilities of writing statements, no leakage of variables, and full access to our class’s internals.

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

泛型类

¥Generic Classes

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

¥Classes, much like interfaces, can be generic. When a generic class is instantiated with new, its type parameters are inferred the same way as in a function call:

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

类可以像接口一样使用泛型约束和默认值。

¥Classes can use generic constraints and defaults the same way as interfaces.

静态成员中的类型参数

¥Type Parameters in Static Members

此代码不合法​​,原因可能并不明显:

¥This code isn’t legal, and it may not be obvious why:

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 成员永远不能引用类的类型参数。

¥Remember that types are always fully erased! At runtime, there’s only one Box.defaultValue property slot. This means that setting Box<string>.defaultValue (if that were possible) would also change Box<number>.defaultValue - not good. The static members of a generic class can never refer to the class’s type parameters.

类运行时的 this

¥this at Runtime in Classes

背景阅读:
这个关键字(MDN)

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

¥It’s important to remember that TypeScript doesn’t change the runtime behavior of JavaScript, and that JavaScript is somewhat famous for having some peculiar runtime behaviors.

JavaScript 对 this 的处理确实不寻常:

¥JavaScript’s handling of this is indeed unusual:

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 而不是类实例。

¥Long story short, by default, the value of this inside a function depends on how the function was called. In this example, because the function was called through the obj reference, its value of this was obj rather than the class instance.

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

¥This is rarely what you want to happen! TypeScript provides some ways to mitigate or prevent this kind of error.

箭头函数

¥Arrow Functions

背景阅读:
箭头函数 (MDN)

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

¥If you have a function that will often be called in a way that loses its this context, it can make sense to use an arrow function property instead of a method definition:

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

这有一些权衡:

¥This has some trade-offs:

  • this 值保证在运行时是正确的,即使对于未使用 TypeScript 检查的代码也是如此

    ¥The this value is guaranteed to be correct at runtime, even for code not checked with TypeScript

  • 这将使用更多内存,因为每个类实例都会有自己的每个以这种方式定义的函数的副本

    ¥This will use more memory, because each class instance will have its own copy of each function defined this way

  • 你不能在派生类中使用 super.getName,因为原型链中没有条目可以从中获取基类方法

    ¥You can’t use super.getName in a derived class, because there’s no entry in the prototype chain to fetch the base class method from

this 参数

¥this parameters

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

¥In a method or function definition, an initial parameter named this has special meaning in TypeScript. These parameters are erased during compilation:

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

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

¥TypeScript checks that calling a function with a this parameter is done so with a correct context. Instead of using an arrow function, we can add a this parameter to method definitions to statically enforce that the method is called correctly:

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

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

¥This method makes the opposite trade-offs of the arrow function approach:

  • JavaScript 调用者可能仍然不正确地使用类方法而没有意识到

    ¥JavaScript callers might still use the class method incorrectly without realizing it

  • 每个类定义只分配一个函数,而不是每个类实例一个

    ¥Only one function per class definition gets allocated, rather than one per class instance

  • 仍然可以通过 super 调用基本方法定义。

    ¥Base method definitions can still be called via super.

this 类型

¥this Types

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

¥In classes, a special type called this refers dynamically to the type of the current class. Let’s see how this is useful:

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 的子类:

¥Here, TypeScript inferred the return type of set to be this, rather than Box. Now let’s make a subclass of Box:

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

你还可以在参数类型注释中使用 this

¥You can also use this in a parameter type annotation:

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

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

¥This is different from writing other: Box — if you have a derived class, its sameAs method will now only accept other instances of that same derived class:

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-based type guards

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

¥You can use this is Type in the return position for methods in classes and interfaces. When mixed with a type narrowing (e.g. if statements) the type of the target object would be narrowed to the specified 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 被验证为真时,这种情况会从框中保存的值中删除 undefined

¥A common use-case for a this-based type guard is to allow for lazy validation of a particular field. For example, this case removes an undefined from the value held inside box when hasValue has been verified to be true:

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

参数属性

¥Parameter Properties

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

¥TypeScript offers special syntax for turning a constructor parameter into a class property with the same name and value. These are called parameter properties and are created by prefixing a constructor argument with one of the visibility modifiers public, private, protected, or readonly. The resulting field gets those modifier(s):

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

类表达式

¥Class Expressions

背景阅读:
类表达式 (MDN)

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

¥Class expressions are very similar to class declarations. The only real difference is that class expressions don’t need a name, though we can refer to them via whatever identifier they ended up bound to:

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

构造函数签名

¥Constructor Signatures

JavaScript 类是使用 new 运算符实例化的。给定类本身的类型,InstanceType 工具类型会对此操作进行建模。

¥JavaScript classes are instantiated with the new operator. Given the type of a class itself, the InstanceType utility type models this operation.

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 类和成员

¥abstract Classes and Members

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

¥Classes, methods, and fields in TypeScript may be abstract.

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

¥An abstract method or abstract field is one that hasn’t had an implementation provided. These members must exist inside an abstract class, which cannot be directly instantiated.

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

¥The role of abstract classes is to serve as a base class for subclasses which do implement all the abstract members. When a class doesn’t have any abstract members, it is said to be concrete.

让我们看一个例子:

¥Let’s look at an example:

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,因为它是抽象的。相反,我们需要创建一个派生类并实现抽象成员:

¥We can’t instantiate Base with new because it’s abstract. Instead, we need to make a derived class and implement the abstract members:

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

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

¥Notice that if we forget to implement the base class’s abstract members, we’ll get an error:

ts
class Derived extends Base {
Non-abstract class 'Derived' does not implement all abstract members of 'Base'18052Non-abstract class 'Derived' does not implement all abstract members of 'Base'
// forgot to do anything
}
Try

抽象构造签名

¥Abstract Construct Signatures

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

¥Sometimes you want to accept some class constructor function that produces an instance of a class which derives from some abstract class.

例如,你可能想编写以下代码:

¥For example, you might want to write this code:

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 的定义,编写这段代码是完全合法的,它最终会构造一个抽象类:

¥TypeScript is correctly telling you that you’re trying to instantiate an abstract class. After all, given the definition of greet, it’s perfectly legal to write this code, which would end up constructing an abstract class:

ts
// Bad!
greet(Base);
Try

相反,你想编写一个接受带有构造签名的东西的函数:

¥Instead, you want to write a function that accepts something with a construct signature:

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 不能。

¥Now TypeScript correctly tells you about which class constructor functions can be invoked - Derived can because it’s concrete, but Base cannot.

类之间的关系

¥Relationships Between Classes

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

¥In most cases, classes in TypeScript are compared structurally, the same as other types.

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

¥For example, these two classes can be used in place of each other because they’re identical:

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

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

¥Similarly, subtype relationships between classes exist even if there’s no explicit inheritance:

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

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

¥This sounds straightforward, but there are a few cases that seem stranger than others.

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

¥Empty classes have no members. In a structural type system, a type with no members is generally a supertype of anything else. So if you write an empty class (don’t!), anything can be used in place of it:

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