Skip to content
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

背景阅读:
类 (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
();
// 打印 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'.
Try

--strictPropertyInitialization

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

ts
class 
BadGreeter
{
name: string;
Property '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
{
// 未初始化,但没有错误
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.
} } const
g
= new
Greeter
();
g
.name = "also not ok";
Cannot assign to 'name' because it is a read-only property.
Try

构造函数

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

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

ts
class 
Point
{
x
: number;
y
: number;
// 带默认值的普通签名 constructor(
x
= 0,
y
= 0) {
this.
x
=
x
;
this.
y
=
y
;
} }
Try
ts
class 
Point
{
x
: number = 0;
y
: number = 0;
// 构造函数重载 constructor(
x
: number,
y
: number);
constructor(
xy
: string);
constructor(
x
: string | number,
y
: number = 0) {
// 代码逻辑 } }
Try

类构造函数签名与函数签名之间只有少数几个区别:

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

超类调用

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

ts
class 
Base
{
k
= 4;
} class
Derived
extends
Base
{
constructor() { // 在 ES5 中打印错误的值;在 ES6 中抛出异常
console
.
log
(this.
k
);
'super' must be called before accessing 'this' in the constructor of a derived class.
super(); } }
Try

在 JavaScript 中忘记调用 super 是一个很容易犯的错误,但 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
() {
// 这里试图修改第 1 行的 'x',而不是类的属性 x = "world";
Type 'string' is not assignable to type 'number'.
} }
Try

Getters / Setters

类也可以有访问器

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

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

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

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

TypeScript 4.3 开始,访问器的 get 和 set 可以具有不同的类型。

ts
class 
Thing
{
_size
= 0;
get
size
(): number {
return this.
_size
;
} set
size
(
value
: string | number | boolean) {
let
num
=
Number
(
value
);
// 不允许 NaN、Infinity 等 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'.
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.
// 注意这里没有错误 return
s
.
toLowerCase
() === "ok";
} }
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'.
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
();
// 基类方法
d
.
move
();
// 派生类方法
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
// 通过基类引用别名化派生实例
const 
b
:
Base
=
d
;
// 没问题
b
.
greet
();
Try

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

ts
class 
Base
{
greet
() {
console
.
log
("Hello, world!");
} } class
Derived
extends
Base
{
// 使此参数变为必需的 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.
console
.
log
(`Hello, ${
name
.
toUpperCase
()}`);
} }
Try

如果我们忽略错误编译这段代码,这个示例将崩溃:

ts
const 
b
:
Base
= new
Derived
();
// 崩溃,因为 "name" 是 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
{
// 不生成 JavaScript 代码, // 仅确保类型正确 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";
} // 打印 "base",而不是 "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
);
// 显式设置原型。
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
() {
// 此处可以访问 protected 成员
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.
Try

protected 成员的暴露

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

ts
class 
Base
{
protected
m
= 10;
} class
Derived
extends
Base
{
// 没有修饰符,因此默认为 'public'
m
= 15;
} const
d
= new
Derived
();
console
.
log
(
d
.
m
); // OK
Try

注意,Derived 已经可以自由地读写 m,因此这并没有实质性地改变这种情况的“安全性”。 这里需要注意的主要一点是,在派生类中,如果这种暴露不是有意的,我们需要小心地重复 protected 修饰符。

跨层级 protected 访问

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

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

这是因为在 Derived2 中访问 x 应该只允许从 Derived2 的子类中进行,而 Derived1 不是其中之一。 此外,如果通过 Derived1 引用访问 x 是非法的(它确实应该是非法的!),那么通过基类引用访问它永远不应改善这种情况。

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

private

private 类似于 protected,但即使从子类中也不允许访问该成员:

ts
class 
Base
{
private
x
= 0;
} const
b
= new
Base
();
// 无法从类外部访问
console
.
log
(
b
.x);
Property 'x' is private and only accessible within class 'Base'.
Try
ts
class 
Derived
extends
Base
{
showX
() {
// 无法在子类中访问
console
.
log
(this.x);
Property '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'.
x
= 1;
}
Try

跨实例 private 访问

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

TypeScript 允许跨实例 private 访问:

ts
class 
A
{
private
x
= 10;
public
sameAs
(
other
:
A
) {
// 没有错误 return
other
.
x
=== this.
x
;
} }
Try

注意事项

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

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

ts
class 
MySafe
{
private
secretKey
= 12345;
}
Try
js
// 在 JavaScript 文件中...
const s = new MySafe();
// 会打印 12345
console.log(s.secretKey);

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

ts
class 
MySafe
{
private
secretKey
= 12345;
} const
s
= new
MySafe
();
// 类型检查期间不允许
console
.
log
(
s
.secretKey);
Property '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 将使用 WeakMap 替代 #

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

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

静态成员

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

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

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

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

ts
class 
MyClass
{
private static
x
= 0;
}
console
.
log
(
MyClass
.x);
Property 'x' is private and only accessible within class 'MyClass'.
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'.
}
Try

为什么没有静态类?

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

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

例如,我们不需要在 TypeScript 中使用“静态类”语法,因为一个普通对象(甚至顶层函数)也能很好地完成工作:

ts
// 不必要的 "static" 类
class 
MyStaticClass
{
static
doSomething
() {}
} // 推荐(替代方案 1) function
doSomething
() {}
// 推荐(替代方案 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!");
Try

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

静态成员中的类型参数

这段代码是不合法的,原因可能不明显:

ts
class 
Box
<
Type
> {
static
defaultValue
: Type;
Static 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
,
}; // 打印 "obj",而不是 "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
;
// 打印 "MyClass" 而不是崩溃
console
.
log
(
g
());
Try

这有一些权衡:

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

this 参数

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

ts
// 带有 'this' 参数的 TypeScript 输入
function 
fn
(
this
:
SomeType
,
x
: number) {
/* ... */ }
Try
js
// JavaScript 输出
function fn(x) {
  /* ... */
}

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

ts
class 
MyClass
{
name
= "MyClass";
getName
(
this
:
MyClass
) {
return this.
name
;
} } const
c
= new
MyClass
();
// OK
c
.
getName
();
// 错误,会崩溃 const
g
=
c
.
getName
;
console
.
log
(g());
The '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) {
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");
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'.
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
;
} else if (
fso
.
isDirectory
()) {
fso
.
children
;
} else if (
fso
.
isNetworked
()) {
fso
.
host
;
}
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
;
if (
box
.
hasValue
()) {
box
.
value
;
}
Try

参数属性

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

ts
class 
Params
{
constructor( public readonly
x
: number,
protected
y
: number,
private
z
: number
) { // 不需要主体 } } const
a
= new
Params
(1, 2, 3);
console
.
log
(
a
.
x
);
console
.
log
(
a
.z);
Property '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");
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.
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'.
// 忘记做任何事情 }
Try

抽象构造签名

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

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

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

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

ts
// 错误!
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.
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
) {
// 不能对 'x' 做任何事,所以我不会做 } // 全部 OK!
fn
(
window
);
fn
({});
fn
(
fn
);
Try