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

与 const、let 和 var 一样,类属性的初始化器将用于推断其类型:

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 的类型会受到 check 的 name: 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 >= ES2022 或 useDefineForClassFields 为 true 时,类字段在父类构造函数完成后初始化,会覆盖父类设置的任何值。当你只想为继承的字段重新声明更准确的类型时,这可能是一个问题。为了处理这些情况,你可以编写 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 值,因为派生类字段初始化尚未运行。

继承内置类型 ​

注意:如果你不打算继承像 Array、Error、Map 等内置类型,或者你的编译目标明确设置为 ES6/ES2015 或更高,你可以跳过本节

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

因此,子类化 Error、Array 等可能不再按预期工作。 这是因为 Error、Array 等的构造函数函数使用 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 ​

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

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

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

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

静态成员也可以使用相同的 public、protected 和 private 可见性修饰符:

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 名称不能使用。 像 name、length 和 call 这样的函数属性不能定义为 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 提供了一种特殊语法,可以将构造函数参数转换为具有相同名称和值的类属性。 这些被称为参数属性,通过在构造函数参数前加上可见性修饰符 public、private、protected 或 readonly 之一来创建。 生成的字段将获得这些修饰符:

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