背景阅读:
类 (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](/tsconfig#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:

  • 构造函数不能有类型参数——这些应该在外部类的声明中指定,我们稍后会学习到
  • 构造函数不能有返回类型注解——始终返回类的实例类型

超级电话

🌐 Super Calls

就像在 JavaScript 中一样,如果你有一个基类,你需要在构造函数体内调用 super();,然后才能使用任何 this. 成员:

🌐 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

请注意,在 JavaScript 中,没有额外逻辑的基于字段的 get/set 对几乎没什么用。 如果在 get/set 操作中不需要添加额外逻辑,公开字段是可以的。

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

🌐 TypeScript has some special inference rules for accessors:

  • 如果存在 get 但没有 set,该属性会自动为 readonly
  • 如果不指定 setter 参数的类型,则从 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

类可以声明索引签名;它们的作用与其他对象类型的索引签名相同(Index Signatures for other object types):

🌐 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

背景阅读:
extends 关键字 (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

背景阅读:
super 关键字 (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:

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

这意味着基类构造函数在自身构造期间看到了 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 或更高版本,则可以跳过本节

在 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 会导致错误。
  • instanceof 将会在子类及其实例之间被打破,所以 (new MsgError()) instanceof MsgError 将返回 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.prototype 复制到 this),但无法修复原型链本身。

成员可见性

🌐 Member Visibility

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

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

public

类成员的默认可见性是 public。 一个 public 成员可以在任何地方访问:

🌐 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 成员仅对子类可见,这些子类是在声明它们的类中继承的。

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,因此这并没有实质性地改变这种情况下的“安全性”。 这里主要需要注意的是,在派生类中,如果这种暴露不是有意的,我们需要小心地重复使用 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

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

🌐 TypeScript doesn’t allow accessing protected members of a sibling class in a class hierarchy:

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 是非法的(它当然应该是!),那么通过基类引用访问它也绝不应该改善这种情况。

🌐 This is 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 on the same topic.

private

private 类似于 protected,但即使是子类也无法访问其成员:

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

不同的面向对象编程语言对于同一个类的不同实例是否可以访问彼此的 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 声明的字段在进行单元测试等操作时可能更容易访问,但缺点是这些字段是 软私有 的,并不能严格保证私密性。

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 的 私有字段 (#) 在编译后仍然是私有的,并且不像之前提到的括号表示法访问那样提供逃逸途径,使它们成为_严格私有_。

🌐 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 将使用 WeakMaps 来代替 #

🌐 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

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

🌐 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)没有像 C# 那样存在名为 static class 的构造。

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

类中的运行时 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 检查的代码也是如此
  • 这将使用更多内存,因为每个类实例都会有自己的每个以这种方式定义的函数的副本
  • 你不能在派生类中使用 super.getName,因为原型链中没有条目可以从中获取基类方法

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 调用者可能仍然不正确地使用类方法而没有意识到
  • 每个类定义只分配一个函数,而不是每个类实例一个
  • 仍然可以通过 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 已被验证为 true 时,此示例会从 box 中的值中移除 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 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

抽象构造签名

🌐 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