背景阅读
类 (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 {
// Overloads
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: any, y?: any) {
// TBD
}
}
Try

类构造函数签名和函数签名之间只有几个区别。

  • 构造函数不能有类型参数 - 这些属于外部类声明,我们将在后面学习。
  • 构造函数不能有返回值类型注解 - 类实例类型始终是返回的内容。

超级调用

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

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 的返回类型推断出来。
  • Getter 和 setter 必须具有相同的成员可见性

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

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 类是简单的查找对象,因此没有“超级字段”的概念。

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'.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'.
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 中,返回对象的构造函数会隐式地将 this 的值替换为 super(...) 的任何调用者的值。生成的构造函数代码需要捕获 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.prototype 复制到 this),但原型链本身无法修复。

成员可见性

可以使用 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访问

不同的面向对象编程语言在是否允许通过基类引用访问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

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

TypeScript站在C#和C++一边,因为在Derived2中访问x应该只允许从Derived2的子类进行,而Derived1不是其中之一。此外,如果通过Derived1引用访问x是非法的(这当然应该是!),那么通过基类引用访问它不应该改善这种情况。

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

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成员,因此派生类无法提高其可见性

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 类型系统中的其他方面一样,`private` 和 `protected` 仅在类型检查期间强制执行

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

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

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

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

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

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

静态成员

背景阅读
静态成员 (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

这有一些权衡

  • this 值在运行时保证是正确的,即使对于没有用 TypeScript 检查的代码也是如此
  • 这将使用更多内存,因为每个类实例将拥有自己定义的每个函数的副本
  • 你不能在派生类中使用 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 中的类、方法和字段可以是抽象的。

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

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

让我们看一个例子

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 文档是一个开源项目。帮助我们改进这些页面 通过发送拉取请求

此页面的贡献者
RCRyan Cavanaugh (60)
OTOrta Therox (15)
HAHossein Ahmadian-Yazdi (6)
Uuid11 (2)
DSDamanjeet Singh (1)
21+

上次更新:2024 年 3 月 21 日