5. 类的用法
约 12731 字大约 42 分钟
2025-05-15
一、简介
1、声明方式
我们一般使用 class
关键词来声明一个类。
// 显式类
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
也可以不指定类型,让 TypeScript 自动推断类型。
class Point {
x;
y;
constructor(x, y) {
this.x = x;
this.y = y;
}
}
类也可以指定初始值。我们可以直接在属性后面指定初始值,也可以像 Javascript 一样,在构造函数的参数列表中指定初始值。
class Point {
x = 0;
y = 0;
constructor(x, y) {
this.x = x;
this.y = y;
}
}
// 或者
class Point {
x: number;
y: number;
constructor(x: number = 0, y: number = 0) {
this.x = x;
this.y = y;
}
}
TypeScript 有一个配置项 strictPropertyInitialization
,只要打开(默认是打开的),就会检查属性是否设置了初值,如果没有就报错。
2、基础成员
类的成员主要以下三种:属性、方法、构造函数。此外,还有一些基于这三种成员扩展的特殊成员,后边会介绍。
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
sepak() {
console.log("Hello, World!");
}
}
如果打开了 strictPropertyInitialization
配置项,就会检查属性实例化时是否有初始值,如果没有就报错。
我们在类中定义的成员,可以在多个地方传入值:定义成员时、构造函数中、实例化后重新赋值。
class Point {
x: number;
y: number = 2;定义成员 y 时指定初始值 z: number;
constructor(x: number) {
this.x = x;构造函数中,手动指定参数为 x 的值 }
}
let point = new Point(1);
console.log(point); // { x:1, y: 2, z: undefined }
point.z = 1; // 我们在类的实例中给 z 赋值实例化后重新赋值
其实方法也是一种特殊的属性,只不过它的类型是函数类型。
如果使用以下写法,需要在实例化的时候,给出具体的实现。
class Demo {
sepak: () => void;
constructor(sepak: () => void) {
this.sepak = sepak;
}
}
let fun_param = () => {
console.log("Hello, World!");
};
let point = new Demo(fun_param);
3、构造函数(constructor
)
类的实例化需要调用类的构造函数,因此每一个类都必须有一个构造函数。构造函数又分为有参构造函数和无参构造函数:
有参构造函数:
不能省略不写,因为实例化要传入参数。
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
let point = new Point(1, 2); // 正确
let point = new Point(); // 错误,缺少参数
无参构造函数
无参构造函数中的成员,可以等到实例化以后再赋值。 我们可以省略不写,TypeScript 会自动帮我们生成。
class Point {
x: number;
y: number;
}
等价于:
class Point {
x: number;
y: number;
constructor() {}
}
实例化:
let point = new Point(); // 正确
point.x = 1;
point.y = 2;
4、存取器(getter
/ setter
)
类的成员还可以包含存取器 accessor
,分为取值器 getter
和存值器 setter
:
getter
:用于读取属性,并可以执行一些处理逻辑,例如:格式转换。setter
:用于写入属性,并可以执行一些处理逻辑,例如:数据验证。
在使用时,我们需要注意以下规则:
- 规则 1:如果某个属性对应的只有
get
,没有set
,则该属性是只读的。 - 规则 2:
get
和set
方法的可访问性修饰符必须相同,要么都是公开的,要么都是私有的。 - 规则 3:在
5.1
版本之前,set
的类型必须兼容get
的的类型,否则报错。5.1
版本之后,这个限制被放宽,允许二者类型不兼容。
下边的例子中,我们定义了一个 userName
属性的 getter
和 setter
方法:
class UserInfo {
_userName = "init value";
get userName() {
console.log("读取 userName 属性:", this._userName);
return this._userName;
}
set userName(value) {
console.log("设置 userName 属性:", value);
this._userName = value;
}
}
// 实例化
let user = new UserInfo();
user.userName = "123"; // 调用 setter
console.log("打印 userName:", user.userName); // 调用 getter
输出:
设置 userName 属性: 123 demo.ts:8
读取 userName 属性: 123 demo.ts:4
打印 userName: 123 demo.ts:15
为什么 5.1
版本要放开限制?
下边代码是关于 DOM 节点的 style
属性的。我们可以看到 CSSStyleDeclaration
是一个对象类型,而 string
是一个字符串类型,它们之间是不兼容的。
interface CSSStyleRule {
/** Always reads as a `CSSStyleDeclaration` */
get style(): CSSStyleDeclaration;
/** Can only write a `string` here. */
set style(newValue: string);
}
element.style; // 返回值为 CSSStyleDeclaration 对象
element.style = "font-size: 20px; color: green;"; // 可以赋值为字符串
因此,在 5.1
版本之后,TypeScript 放宽了这个限制,允许 set
和get
的类型不兼容,但是需要像前边的 CSSStyleRule
接口一样,在定义属性的时候 显式指定 对应类型。
5、属性索引
类允许定义属性索引,可以定义属性和方法的索引签名。
下面示例中,[prop:string]
表示所有属性名类型为字符串的属性,它们的属性值要么是布尔值,要么是返回布尔值的函数。
class MyClass {
[prop: string]: boolean | ((prop: string) => boolean);
getResultById(value: string) {
return value === "abc";
}
}
let a: MyClass = {
prop1: true,
prop2: (val: string) => val.length > 0,
getResultById(val: string) {
return val === "abc";
},
};
注意:类的方法实际上是一种特殊的属性,即属性值为函数的属性,所以属性索引的类型定义也涵盖了方法。如果一个对象同时定义了属性索引和方法,那么属性索引必须包含方法的类型。
class MyClass {
[prop: string]: boolean;
getResultById(value: string) {Property 'getResultById' of type '(value: string) => value is "abc"' is not assignable to 'string' index type 'boolean'. return value === "abc";
}
}
6、可访问性修饰符
类的内部成员的外部可访问性,由三个可访问性修饰符(Access Modifiers)控制:public
、private
和 protected
。
public
(默认):成员可以被类的内部、子类、实例访问。如果省略不写,则默认为public
。protected
:成员可以在类的内部和子类中访问。private
:成员只能在类的内部访问。
ES2022 引入了新的私有成员写法 #propName
,它的作用和 private
基本相同,我们会在后文中介绍。
他们的区别:
成员特性 | public | protected | private |
---|---|---|---|
类的外部 | 无法访问 | 无法访问 | 无法访问 |
类的内部 | 可以访问 | 可以访问 | 可以访问 |
类的实例 | 可以访问 | 无法访问 | 无法访问 |
子类内部 | 可以访问 | 可以访问 | 无法访问 |
子类实例 | 可以访问 | 无法访问 | 无法访问 |
1. public
公开的
public
修饰符表示该成员是公开成员。
类的内部、类的实例对象、子类内部、子类的实例可以 访问这个成员,外部无法访问。
class A {
public x = "x";
}
class B extends A {
public y = "y";
get_paren_x() {
return this.x;
}
}
const b = new B();
b.x; // "x"
b.y; // "y"
b.get_paren_x(); // "x"
B.y; // 报错 类型“typeof A”上不存在属性“x”。
上述代码中,父类 A
的属性 x
是公开成员,子类 B
的属性 y
是公开成员。子类 B
可以访问父类 A
的公开成员 x
,类的实例 b
也可以访问父类 A
的公开成员 x
。
2. protected
受保护的
protected
修饰符表示该成员是受保护成员。
类的内部、子类内部可以访问这个成员,类的实例对象、子类的实例无法访问这个成员,外部无法访问这个成员。
class A {
protected x = "x";
}
class B extends A {
protected y = "y";
get_paren_x() {
return this.x;
}
}
const b = new B();
b.get_paren_x(); // "x"
b.y; // 报错 类型“B”上不存在属性“y”。B.x; // 报错 类型“typeof A”上不存在属性“x”。Property 'y' is protected and only accessible within class 'B' and its subclasses.
3. private
私有的
private
修饰符表示该成员是私有成员。
类的内部可以访问这个成员,类的实例对象、子类的实例无法访问这个成员,外部无法访问这个成员。
class A {
private x = "x";
}
class B extends A {
private y = "y";
get_paren_x() {
return this.x; // 报错 属性“x”为私有属性,只能在类“A”中访问。Property 'x' is private and only accessible within class 'A'. }
}
const b = new B();
b.y; // 报错 类型“B”上不存在属性“y”。Property 'y' is private and only accessible within class 'B'.
构造方法也可以是私有的。一般用来实现 单例模式
- 把构造方法设置为私有成员,这样类就无法被
new
命令实例化。 - 通过使用静态方法充当工厂函数,判断实例对象是否已创建?如果实例对象已创建直接返回该实例,如果未创建则新建一个实例对象并返回。
例如以下代码: Singleton
类的实例无法通过 new
命令获得,只能通过 getInstance()
方法获得,并且每次使用都会返回同一个实例,这就确保了 Singleton 类只能创建一个实例。
class Singleton {
private static instance?: Singleton;
private constructor() {}
static getInstance() {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}
const s = Singleton.getInstance();
7、实例属性的简写形式
传统写法中,同一个属性要声明两次类型,一次在类的头部,另一次在构造方法的参数里面。在声明了属性之后,还需要在构造方法里面手动赋值,这看起来有点累赘。
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
下面的示例中,构造方法的参数 a
前面有 public
修饰符,这时 TypeScript 就会自动声明一个公开属性 a
,不必在构造方法里面写任何代码,同时还会设置a
的值为构造方法的参数值。注意,这里的 public
不能省略。
class Person {
constructor(
public a: number, // 自动生成并初始化 this.a
protected b: number, // 自动生成受保护属性 this.b
private c: number, // 自动生成私有属性 this.c
readonly d: number // 自动生成只读属性 this.d
) {}
}
编译结果:
class A {
a;
b;
c;
d;
constructor(a, b, c, d) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
}
}
上面示例中,从编译结果可以看到,构造方法的 a
、b
、c
、d
会生成对应的实例属性。
8、#prop
私有成员(新提案)
1. private
存在的问题
严格地说,private
定义的私有成员,并不是真正意义的私有成员。
因为代码被编译成 JavaScript 后,private
关键字就被剥离了,这时外部访问该成员就不会报错。
同样,TypeScript 对于访问 private
成员没有严格禁止,使用方括号写法([]
)或者 in
运算符,实例对象就能访问该成员。
class A {
private x = 1;
}
const a = new A();
if ("x" in a) {
console.log(a["x"]); // 1
}
2. #propName
的出现
由于 private
存在这些问题,ES2022 标准 引入了新的私有成员写法 #propName
。使用 ES2022 的写法,可以获得真正意义上的私有成员。
#propName
写法的私有成员只能在当前类的内部使用。它无法被外界访问、无法被实例对象访问、无法被子类继承、无法被子类通过 super
关键字访问。
例如下述代码中:
class A {
#name = "Alice";
getName() {
return this.#name;
}
}
class B extends A {
getNameA() {
return super.#name;Property '#name' is not accessible outside class 'A' because it has a private identifier. }
}
const a = new A();
console.log(a.getName()); // Alice
console.log(a["#name"]); // 报错Element implicitly has an 'any' type because expression of type '"#name"' can't be used to index type 'A'.
Property '#name' does not exist on type 'A'.
3. #propName
允许重名的私有成员
private
定义的私有成员,是不允许出现重名的。而 #propName
写法允许,它们是完全不同的两个成员,互不干扰。
class A {
#name = "Alice";
getName() {
return this.#name;
}
}
class B extends A {
#name = "Bob";
getNameB() {
return this.#name;
}
}
const a = new A();
const b = new B();
console.log(a.getName());
console.log(b.getName());
console.log(b.getNameB());
输出:
Alice demo.ts:15
Alice demo.ts:16
Bob demo.ts:17
9、可选属性 ?
在定义属性时,我们可以使用 ?
来表示该属性是可选的,同时需要在构造函数中定义这个可选参数。
如果类中包含构造函数,一定要在参数列表中使用 ?
标记哪个是可选属性,同时要放在构造函数参数列表的最后面。
class b {
constructor(public d: string, public e: string, public f?: string) {}
}
let a = new b("a", "b");
10、只读 readonly
readonly
修饰符用于指定属性一旦被实例化后,不能在其他地方修改。
class Point {
readonly x: number;
readonly y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
let point = new Point(1, 2);
point.x = 3; // 报错Cannot assign to 'x' because it is a read-only property.
readonly
修饰符可以与 static
、可访问性修饰符一起使用,例如:
class Point {
public readonly a: number = 1;
protected readonly b: number = 1;
private readonly c: number = 1;
static readonly c: number = 1;
}
也可以使用属性简写形式,例如:
class Point {
constructor(
readonly abc: number = 1,
public readonly x: number = 2,
protected readonly y: number = 3,
private readonly z: number = 4
) {}
}
11、静态成员 static
1. static
关键字
静态成员是只能通过类调用的成员,不能通过实例对象使用。静态成员可以是属性、方法、属性索引。例如:Array.from()
、Array.isArray()
、Math.PI
都是静态成员。
类的内部通过 static
关键字,定义静态成员。
class MyClass {
static x = 0;
static printX() {
console.log(MyClass.x); // 通过类访问静态成员
}
getX() {
return this.x; // 类的内部无法通过 this 访问静态成员Property 'x' does not exist on type 'MyClass'. Did you mean to access the static member 'MyClass.x' instead? }
}
MyClass.x; // 0
MyClass.printX(); // 0
let myClass = new MyClass();
myClass.x; // 报错Property 'x' does not exist on type 'MyClass'. Did you mean to access the static member 'MyClass.x' instead?myClass.printX(); // 报错Property 'printX' does not exist on type 'MyClass'. Did you mean to access the static member 'MyClass.printX' instead?
2. 结合可访问性修饰符
static
关键字前面可以使用 public
、private
、protected
修饰符。
public
的静态成员可以通过类调用,也可以被子类继承。
class A {
public static x = 0;
}
class B extends A {}
A.x; // 0
B.x; // 0
public
的静态成员可以通过类调用,也可以被子类继承。
class A {
public static x = 0;
}
class B extends A {}
A.x; // 0
B.x; // 0
protected
的静态成员不可以通过类调用,但是可以被子类继承。
class A {
protected static x = 1;
}
class B extends A {
static getX() {
return B.x;
}
}
A.x; // 报错Property 'x' is protected and only accessible within class 'A' and its subclasses.B.x; // 报错Property 'x' is protected and only accessible within class 'A' and its subclasses.B.getX(); // 1
private
的静态成员不可以在外部通过类调用,也不可以被子类继承,只能在类内部调用。
下述代码中,静态属性 x
前面有 MAX_VALUE
修饰符,表示只能在 Utils
类内部使用,如果在外部调用这个属性就会报错。
class Utils {
private static MAX_VALUE = 100;
static add(a: number, b: number) {
if (a + b > Utils.MAX_VALUE) {
return Utils.MAX_VALUE;
} else {
return a + b;
}
}
}
class MathUtils extends Utils {}
Utils.add(1, 2); // 3
Utils.add(100, 2); // 100
Utils.MAX_VALUE; // 报错Property 'MAX_VALUE' is private and only accessible within class 'Utils'.MathUtils.MAX_VALUE; // 报错Property 'MAX_VALUE' is private and only accessible within class 'Utils'.
3. #prop
静态私有成员
静态私有属性也可以用 ES6 语法的 #
前缀表示,上面示例可以改写如下。
class MyClass {
static #x = 0;
}
4. 常量(静态只读成员)
静态成员也可以使用 readonly
修饰符,表示只读属性,不能修改,一般用作类的常量,例如 Math.PI
、Math.E
。
class MyClass {
static readonly x = 0;
}
MyClass.x; // 0
MyClass.x = 1; // 报错
12、匿名类(类表达式)
匿名类是指没有名称的类。一般使用类表达式来定义匿名类。
// 匿名类
let Point = class {
x: number;
y: number;
};
显式声明适合:
- 需要多处重用的核心业务逻辑
- 需要清晰类型定义和继承体系
- 需要良好调试体验的生产代码
- 作为公共 API 的一部分
隐式声明适合:
- 一次性实现或临时用途
- 需要隐藏实现细节的场景
- 实现接口的简单方式
- 策略模式中的策略实现
- 闭包中的私有实现
二者区别:
特性 | 显式类 | 匿名类 |
---|---|---|
类名 | 有正式类名(如 Person) | 无正式类名,只有变量名 |
类型系统 | 类名即类型(Person 类型) | 结构类型(通过变量名引用) |
可访问性 | 可在定义前引用(hoisting) | 必须先定义后使用 |
调试信息 | 显示实际类名 | 显示为 class 或变量名 |
继承 | 可直接继承(class Student extends Person) | 只能通过变量名继承 |
导出/导入 | 可直接导出类 | 只能导出变量 |
构造函数名 | 与类名相同 | 显示为 "class" |
三、类的继承
类可以继承另外一个类,即子类继承父类。子类可以继承父类的所有非私有成员,包括属性、方法、构造函数。子类无需再定义这些成员,可以直接使用父类的成员。
在具体的处理上,我们需要遵循很多的规则,但是总结起来就是为了满足 结构类型原则。
总结起来,规则如下
- 类只能继承类,类是构造函数的一种语法糖,所以类也可以继承构造函数。
- 一个类只能继承一个父类,但是每个类可以被多个子类继承。
- 父类有的属性,子类可以有,也可以保持不变,也可以类型更严格
- 父类中的可选成员,在子类中可以变成必选,但是必选成员不能变成可选成员。
- 父类中成员的可访问性,在子类中可以保持不变,也可以变成更宽松的可访问性。
1、基本形式
类可以使用 extends
关键字继承另一个类,以此来获取父类所有的非私有成员。
class A {
greet() {
console.log("Hello, world!");
}
}
class B extends A {}
const b = new B();
b.greet(); // "Hello, world!"
上面示例中,子类 B
继承了基类 A
,因此就拥有了 greet()
方法,不需要再次在类的内部定义这个方法了。
根据结构类型原则,A
的结构与 B
的结构兼容。因此,子类 B
可以用来给父类 A
赋值。
2、成员类型的处理
在子类中,子类的属性类型可以和父类保持一致,也可以严格,也可以增加父类没有的属性。
例如下边的代码中:
- 子类
B
的属性name
的 类型 是A
的子类型。类型定义更加严格 - 子类
B
的方法show
的 返回值类型 是A
的子类型、参数类型 则相反。类型定义更加严格 - 子类
B
的允许添加父类A
没有的属性age
class A {
name: string | number;
show(value: string | number): string | number {
return value;
}
}
class B extends A {
declare name: string;
age: number;
show(value: string): string {
return value;
}
}
函数的参数类型允许双变
传统写法的函数定义,是允许参数类型双变的,即可以更严格,也可以更宽松。但是函数表单时类型的函数定义,只允许参数类型逆变,即只能更严格。
例如 speak()
为函数表达式写法,只能是逆变,hold()
和 breath()
的写法支持双变。
class Parent {
name: string;
}
class Child extends Parent {
age: number;
}
class Person {
speak = (val: Child) => {
return "speak";
};
hold(val: Child) {
return "hold";
}
breath(val: Parent) {
return "breath";
}
}
class Man extends Person {
speak = (val: Parent) => {
return "speak";
};
hold(val: Parent) {
return "hold";
}
breath(val: Child) {
return "breath";
}
}
3、可访问性的处理
子类中,成员的可访问性也可以和父类保持一致,也可以更宽松。但是子类不能定义父类的同名私有成员。
主要表现在以下几个方面:
- 父类中是
public
,在子类中只能为public
,不可以被改变。 - 父类中是
protected
,在子类中可以为public
和protected
,可以被提升。 - 子类不可以定义父类中已存在的同名私有
private
成员。
class Parent {
public a: string = "a";
protected b: string = "b";
private c: string = "c";
}
class Child1 extends Parent {Class 'Child1' incorrectly extends base class 'Parent'.
Property 'a' is protected in type 'Child1' but public in type 'Parent'. protected declare a: string;
}
class Child2 extends Parent {
public declare b: string;
}
class Child3 extends Parent {Class 'Child3' incorrectly extends base class 'Parent'.
Types have separate declarations of a private property 'c'. private declare c: string;
}
4、可选属性的处理
子类可以继承父类的可选属性,因此子类中无需定义这些属性,也可以不传入这些属性。父类的可选属性在子类中可以保持不变,也可以变成必选,但是必选属性不能变成可选属性。
如果子类中定义了构造器,在调用 super()
的时候,需要注意参数的可选性处理。
class b {
constructor(public a: string, public b?: string) {}
}
class c extends b {
constructor(public readonly d: string, public e?: string, public f?: string) {
super(d); // 这里可以决定可选属性要不要在父类的构造函数中完成初始化。
// 或者 super(d, e);
// 需要注意父类的a 是必选参数,不能将 e、f 传给父类的必选参数 a
}
}
5、只读属性的处理
结构类型原则只要求父子类的结构兼容,对于只读性没有要求。
因此,子类可以继承父类的只读属性,也可以在子类中重新定义修改为可读写的属性。
class A {
readonly name: string;
age: number;
}
class B extends A {
declare name: string;
declare readonly age: number;
}
6、super
关键字
在 TypeScript 中,子类调用父类构造函数的过程,就是子类继承父类属性和方法的过程。而 super
就是用于访问和调用父类的属性和方法的一个重要关键字,它在类继承中扮演着重要角色。
super
关键字的只能用来访问父类的方法 super.method()
和构造函数 super()
, TypeScript 规定不允许通过 super
关键字访问父类的属性。
为什么将 super()
设计为不允许访问父类的属性?
因为这种限制鼓励更好的封装性,强制子类通过方法来与父类交互,而不是直接访问父类的内部状态。并且一旦涉及到需要通过 super
的场景,一定是子类和父类的实现不一致,而类中的属性只有定义没有实现,方法和构造函数都有定义和实现。
1. 访问父类的构造函数
关于 super()
是否可以省略,有以下规则:
当类缺少构造函数定义,TypeScript 会自动添加一个构造函数。在继承的过程中,TypeScript 还能自动在子类中添加 super()
,这个 super()
可能带参数,也可能不带参数,取决于父类的构造函数是否有参数。因为子类能够继承父类的非私有成员,
或者就记住一句话:只要子类中出现了显式构造函数,就必须添加 super()
。
关于 super()
是否要带参数,有以下规则:
父类是有参构造函数,子、孙类只要手动调用 super()
,就必须传入参数,且类型必须兼容、数量不能少。
父类实例化需要参数,子、孙类实例化也必须传入参数,且类型必须兼容、数量不能少。
如果不满足以上条件:子类和父类就无法保持类型兼容。
如下列代码中,完整演示了上述规则:
class A {
a: string;
}
class B extends A {
b: string;
constructor(b: string) {
super(); // B 类的构造函数中必须调用 super()
this.b = b;
}
}
class C extends B {
// C 类可以省略 super()
c: string;
}
class D extends C {
// 此处为实例属性简写形式
constructor(public d: string, public e: string) {
super(d); // D 类不仅得添加 super(),还得传入参数
}
}
let a = new A();
let b = new B("B");
let c = new C("C");
let d = new D("D", "E");
console.log(a); // {}
console.log(b); // { b: "B" }
console.log(c); // { b: "C" }
console.log(d); // { b: "D", d: "D", e: "E" }
输出:
A {}
B { b: 'B' }
C { b: 'C' }
D { b: 'D', d: 'D', e: 'E' }
2. 访问父类的方法
super.method
用于调用父类的方法,但不能直接访问父类的属性(除非通过 父类的公开方法间接访问)。
class Parent {
name = "Parent";
getName() {
return this.name;
}
}
class Child extends Parent {
logParentName() {
console.log(super.name); // 父类定义的类字段“name”无法通过 super 在子类中访问。ts(2855)
console.log(super.getName()); // ✅ 正确(通过方法访问)
}
}
3. 访问父类的静态方法
super.method
可以用来访问父类的静态方法,但是有限制。只能在静态方法中访问父类的静态方法,不能在实例方法中访问父类的静态方法。
class Parent {
static greet() {
return "Hello from Parent";
}
}
class Child extends Parent {
static greet() {
return super.greet() + " and Child"; // ✅ 调用父类静态方法
}
}
console.log(Child.greet()); // "Hello from Parent and Child"
4. 箭头函数中的 super
箭头函数没有自己的 super
绑定,它会从外层作用域继承 super
。在类方法中使用箭头函数时需谨慎,可能编译报错。
class Parent {
method() {
return "Parent";
}
}
class Child extends Parent {
arrowMethod = () => {
console.log(super.method()); // ❌ 可能报错(取决于编译目标)
};
}
7、方法重写、 override
有些时候,我们继承他人的类,可能会在不知不觉中覆盖了他人的方法。为了防止这种情况,TypeScript 4.3 引入了 override 关键字,可以帮我们表明我们的意图:覆盖父类的方法。
class B extends A {
override show() {}
}
上面示例中,B
类的 show()
方法前面加了 override
关键字,明确表明作者的意图,就是要覆盖 A
类里面的同名方法。这时,如果 A
类没有定义自己的 show()
方法,就会报错。
但是,这依然没有解决 无意中覆盖 的问题。因此,TypeScript 又提供了一个编译参数 noImplicitOverride
。一旦打开这个参数,子类覆盖父类的同名方法就会报错,除非使用了 override
关键字。
8、类继承 Constructor
构造函数
extends
关键字后面不一定是类名,可以是一个表达式,只要它的类型是构造函数就可以了。而类其实就是构造函数的一种语法糖。
// 例一
class MyArray extends Array<number> {}
// 例二
class MyError extends Error {}
// 例三
class A {
greeting() {
return "Hello from A";
}
}
class B {
greeting() {
return "Hello from B";
}
}
interface Greeter {
greeting(): string;
}
interface GreeterConstructor {
new (): Greeter;
}
function getGreeterBase(): GreeterConstructor {
return Math.random() >= 0.5 ? A : B;
}
class Test extends getGreeterBase() {
sayHello() {
console.log(this.greeting());
}
}
四、被接口继承
类可以被接口继承,即接口可以继承类的成员、方法、属性、静态成员。
下边的示例中,接口 B
继承了类 A
,因此接口 B
就具有属性 x
、y()
和 z
,B
接口的实例化对象 b
就需要实现这些属性。
class A {
x: string = "";
y(): boolean {
return true;
}
}
interface B extends A {
z: number;
}
const b: B = {
x: "",
y: function () {
return true;
},
z: 123,
};
interface
还可以继承 class
,即继承该类的所有的成员的定义。但是实际开发中,我们需要限制 接口只能继承类的公开成员。
接口可以继承类的私有成员和保护成员,但是下边的例子告诉我们:无论实现类是否实现了接口继承来的私有成员和保护成员,都会报错。
class A {
constructor(protected a: string, private b: string) {}
}
interface B extends A {
c: string;
}
class C implements B {Class 'C' incorrectly implements interface 'B'.
Type 'C' is missing the following properties from type 'B': a, b // Error:c 缺少 a 和 b 的实现
constructor(public c: string) {}
}
class D implements B {Class 'D' incorrectly implements interface 'B'.
Property 'a' is protected but type 'D' is not a class derived from 'A'. // Error:D 不是 A 的 子类
constructor(public c: string, protected a: string, private b: string) {}
}
五、类的实现
interface
定义了类的约束条件,相当于类的对外接口,包含了实例对外公开的属性和方法,因此接口里不能定义私有的属性和方法。
TypeScript 设计者认为,私有属性是类的内部实现,接口作为模板,不应该涉及类的内部代码写法
1、implements
关键字
interface
接口或 type
别名,可以用对象的形式,为 class
指定一组检查条件。然后,类使用 implements
关键字,表示当前类满足这些检查条件的限制。
类只能实现对象类型,interface
默认是对象类型,但 type
可能不是,需要注意。
interface Country {
name: string;
capital: string;
}
// 或者
type Country = {
name: string;
capital: string;
};
class MyCountry implements Country {
name = "";
capital = "";
}
2、实现规则
接口可以被多个类实现,可以理解为,每一个类都有对应的约束条件,接口是这些约束条件的超集。
由此,不难理解:在定义实现类时,可以保持约约束条件不变,也可以收窄约束条件。
总结起来,规则如下
- 接口包含的成员(除去可选成员
?
外),都应该被被类实现,不可缺少。 - 接口中的可选成员
?
,可以在实现类中被忽略,也可以被实现,也可以修改为必选,但是不能必选变可选。 - 实现类可以添加接口里没有的成员,这些成员不受接口定义的约束。
- 属性的类型定义收窄:从兼容性来看,实现类的属性类型是接口中定义的类型的子类型。
- 方法的返回值类型收窄,表现和属性类型收窄一样。
- 方法的参数数量,可以一致,可以减少,但是不能增加。
- 方法的参数类型默认下是逆变,只能收窄,不能变宽。但是特殊条件下支持双变,既允许放宽,也允许收窄。
下边代码中,我们们定义了一个接口 Person
、两个具备继承关系的类 Parent
和 Child
。
class Parent {
name: string;
}
class Child extends Parent {
age: number;
}
interface Person {
name: string;
age: string | number;
height?: number;
skills: Parent;
walk: (val: number) => string | number;
run: (val: number) => string;
sleep: (val: number, val2: number) => string;
speak: (val: Child) => string;
stand: (val: number) => Parent;
breath(val: Parent): string;
hold(val: Child): string;
}
class Man implements Person {
// @annotate: name 保持不变
name: string;
// @annotate: age 类型 number 为 string | number 的子类型
age: number;
// @annotate: height 由可选变成必选
height: number;
// @annotate: skills 类型 Child 为 Parent 的子类型
skills: Child;
// @annotate: gender 新增的额外成员
gender: string;
// @annotate: walk 的返回值 为 string | number 的子类型
override walk: (val: number) => string;
// @annotate: run 的参数 val 为 number 的父类型
override run: (val: number | string) => string;
// @annotate: sleep 的参数的参数数量变少
override sleep: (val: number) => string;
// @annotate: speak 的参数 val 为 Child 的父类型
override speak: (val: Parent) => string;
// @annotate: stand 的返回值 为 Parent 的子类型
override stand: (val: number) => Child;
// @annotate: breath 的参数 val 为 Parent 的子类型 双变
override breath(val: Child) {
return "breath";
}
// @annotate: hold 的参数 val 为 Parent 的父类型
override hold(val: Parent) {
return "hold";
}
}
经上述代码,我们得出结论:类的实现就是对接口的约束条件,放宽输入,收窄输出。
3、实现多个接口
类可以实现多个接口(其实是接受多重限制),每个接口之间使用逗号分隔。
class Car implements MotorVehicle, Flyable, Swimmable {
// ...
}
上面示例中,Car
类同时实现了MotorVehicle
、Flyable
、Swimmable
三个接口。这意味着,它必须部署这三个接口声明的所有属性和方法,满足它们的所有条件。
同名成员的处理与同名接口合并的处理类似。
在实现多个接口时,很容易会遇到同名成员的问题,我们要求接口之间必须满足以下规则:
- 同名属性要求类型必须相同,否则会报错。
- 同名方法允许定义不一致,以方法重载的方式解决。重载可以用兼容性更强的方法来实现,也可以直接实现重载。
但是,同时实现多个接口并不是一个好的写法,容易使得代码难以管理,可以两种方案来替代:
第一种是使用接口合并,将多个接口合并成一个接口。
type Car = MotorVehicle & Flyable & Swimmable;
class SUV implements Car {
/** some code **/
}
上面示例中,Car
是 MotorVehicle
、Flyable
、Swimmable
三个接口的合并。实现 Car
就等于实现这三个接口。
第二种写法是接口的继承:
interface SuperCar extends MotorVehicle, Flyable, Swimmable {
// ...
}
class SecretCar implements SuperCar {
// ...
}
上面示例中,类SecretCar
通过SuperCar
接口,就间接实现了多个接口。
4、类与接口的合并
TypeScript 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类。
class A {
x: number = 1;
}
interface A {
y: number;
}
let a = new A();
a.y = 10;
a.x; // 1
a.y; // 10
上面示例中,类A
与接口A
同名,后者会被合并进前者的类型定义。
注意,合并进类的非空属性(上例的y
),如果在赋值之前读取,会返回undefined
。
class A {
x: number = 1;
}
interface A {
y: number;
}
let a = new A();
a.y; // undefined
上面示例中,根据类型定义,y
应该是一个非空属性。但是合并后,y
有可能是undefined
。
六、类作为类型
1、实例类型
TypeScript 的类本身就是一种类型,但是它代表该类的实例类型,而不是 class 的自身类型。
class Color {
name: string;
constructor(name: string) {
this.name = name;
}
}
const green: Color = new Color("green");
上面示例中,定义了一个类 Color
。它的类名就代表一种类型,实例对象 green
就属于该类型。
对于引用实例对象的变量来说,既可以声明类型为 Class,也可以声明类型为 Interface,因为两者都代表实例对象的类型。
interface MotorVehicle {}
class Car implements MotorVehicle {}
// 写法一
const c1: Car = new Car();
// 写法二
const c2: MotorVehicle = new Car();
上面示例中,变量的类型可以写成类 Car
,也可以写成接口 MotorVehicle
。它们的区别是,如果类 Car
有接口 MotorVehicle
没有的属性和方法,那么只有变量 c1
可以调用这些属性和方法。
作为类型使用时,类名只能表示实例的类型,不能表示类的自身类型。
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
// 错误
function createPoint(PointClass: Point, x: number, y: number) {
return new PointClass(x, y);
}
上面示例中,函数 createPoint()
的第一个参数 PointClass
,需要传入 Point 这个类,但是如果把参数的类型写成 Point
就会报错,因为 Point
描述的是实例类型,而不是 Class 的自身类型。
由于类名作为类型使用,实际上代表一个对象,因此可以把类看作为对象类型起名。事实上,TypeScript 有三种方法可以为对象类型起名:type
、interface
和 class
。
2、类的自身类型
要获得一个类的自身类型,一个简便的方法就是使用 typeof
运算符。
function createPoint(PointClass: typeof Point, x: number, y: number): Point {
return new PointClass(x, y);
}
上面示例中,createPoint()
的第一个参数 PointClass
是 Point
类自身,要声明这个参数的类型,简便的方法就是使用 typeof Point
。因为 Point
类是一个值,typeof Point
返回这个值的类型。注意,createPoint()
的返回值类型是 Point
,代表实例类型。
JavaScript 语言中,类只是构造函数的一种语法糖,本质上是构造函数的另一种写法。所以,类的自身类型可以写成构造函数的形式。
function createPoint(
PointClass: new (x: number, y: number) => Point,
x: number,
y: number
): Point {
return new PointClass(x, y);
}
上面示例中,参数 PointClass
的类型写成了一个构造函数,这时就可以把 Point
类传入。
构造函数也可以写成对象形式,所以参数 PointClass
的类型还有另一种写法。
function createPoint(
PointClass: {
new (x: number, y: number): Point;
},
x: number,
y: number
): Point {
return new PointClass(x, y);
}
根据上面的写法,可以把构造函数提取出来,单独定义一个接口(interface),这样可以大大提高代码的通用性。
interface PointConstructor {
new (x: number, y: number): Point;
}
function createPoint(
PointClass: PointConstructor,
x: number,
y: number
): Point {
return new PointClass(x, y);
}
总结一下,类的自身类型就是一个构造函数,可以单独定义一个接口来表示。
七、结构类型原则
Class 也遵循“结构类型原则”。一个对象只要满足 Class 的实例结构,就跟该 Class 属于同一个类型。
class Foo {
id!: number;
}
function fn(arg: Foo) {
// ...
}
const bar = {
id: 10,
amount: 100,
};
fn(bar); // 正确
上面示例中,对象 bar
满足类 Foo
的实例结构,只是多了一个属性 amount
。所以,它可以当作参数,传入函数 fn()
。
如果两个类的实例结构相同,那么这两个类就是兼容的,可以用在对方的使用场合。
class Person {
name: string;
}
class Customer {
name: string;
}
// 正确
const cust: Customer = new Person();
上面示例中,Person
和 Customer
是两个结构相同的类,TypeScript 将它们视为相同类型,因此 Person
可以用在类型为 Customer
的场合。
现在修改一下代码,Person
类添加一个属性。
class Person {
name: string;
age: number;
}
class Customer {
name: string;
}
// 正确
const cust: Customer = new Person();
上面示例中,Person
类添加了一个属性 age
,跟 Customer
类的结构不再相同。但是这种情况下,TypeScript 依然认为,Person
属于 Customer
类型。
这是因为根据“结构类型原则”,只要 Person
类具有 name
属性,就满足 Customer
类型的实例结构,所以可以代替它。反过来就不行,如果 Customer
类多出一个属性,就会报错。
class Person {
name: string;
}
class Customer {
name: string;
age: number;
}
const cust: Customer = new Person(); // 报错
上面示例中,Person
类比 Customer
类少一个属性 age
,它就不满足 Customer
类型的实例结构,就报错了。因为在使用 Customer
类型的情况下,可能会用到它的 age
属性,而 Person
类就没有这个属性。
总之,只要 A
类具有 B
类的结构,哪怕还有额外的属性和方法,TypeScript 也认为 A
兼容 B
的类型。
不仅是类,如果某个对象跟某个 class
的实例结构相同,TypeScript 也认为两者的类型相同。
class Person {
name: string;
}
const obj = { name: "John" };
const p: Person = obj; // 正确
上面示例中,对象 obj
并不是 Person
的实例,但是赋值给变量 p
不会报错,TypeScript 认为 obj
也属于 Person
类型,因为它们的属性相同。
由于这种情况,运算符 instanceof
不适用于判断某个对象是否跟某个 class
属于同一类型。
obj instanceof Person; // false
上面示例中,运算符 instanceof
确认变量 obj
不是 Person
的实例,但是两者的类型是相同的。
空类不包含任何成员,任何其他类都可以看作与空类结构相同。因此,凡是类型为空类的地方,所有类(包括对象)都可以使用。
class Empty {}
function fn(x: Empty) {
// ...
}
fn({});
fn(window);
fn(fn);
上面示例中,函数 fn()
的参数是一个空类,这意味着任何对象都可以用作 fn()
的参数。
注意,确定两个类的兼容关系时,只检查实例成员,不考虑静态成员和构造方法。
class Point {
x: number;
y: number;
static t: number;
constructor(x: number) {}
}
class Position {
x: number;
y: number;
z: number;
constructor(x: string) {}
}
const point: Point = new Position("");
上面示例中,Point
与 Position
的静态属性和构造方法都不一样,但因为 Point
的实例成员与 Position
相同,所以 Position
兼容 Point
。
如果类中存在私有成员(private)或保护成员(protected),那么确定兼容关系时,TypeScript 要求私有成员和保护成员来自同一个类,这意味着两个类需要存在继承关系。
// 情况一
class A {
private name = "a";
}
class B extends A {}
const a: A = new B();
// 情况二
class A {
protected name = "a";
}
class B extends A {
protected name = "b";
}
const a: A = new B();
上面示例中,A
和 B
都有私有成员(或保护成员) name
,这时只有在 B
继承 A
的情况下(class B extends A
),B
才兼容 A
。
八、抽象类、抽象成员
TypeScript 允许在类的定义前面,加上关键字 abstract
,表示该类不能被实例化,只能当作其他类的模板。这种类就叫做“抽象类”(abstract class)。
抽象类是类的基类,不能直接实例化,只能继承和被继承。相当于类的模板。
abstract class A {
id = 1;
}
const a = new A(); // 报错
上面示例中,直接新建抽象类的实例,会报错。
抽象类只能当作基类使用,用来在它的基础上定义子类。
abstract class A {
id = 1;
}
class B extends A {
amount = 100;
}
const b = new B();
b.id; // 1
b.amount; // 100
上面示例中,A
是一个抽象类,B
是 A
的子类,继承了 A
的所有成员,并且可以定义自己的成员和实例化。
抽象类的子类也可以是抽象类,也就是说,抽象类可以继承其他抽象类。
abstract class A {
foo: number;
}
abstract class B extends A {
bar: string;
}
抽象类的内部可以有已经实现好的属性和方法,也可以有还未实现的属性和方法。后者就叫做“抽象成员”(abstract member),即属性名和方法名有 abstract
关键字,表示该方法需要子类实现。如果子类没有实现抽象成员,就会报错。
abstract class A {
abstract foo: string;
bar: string = "";
}
class B extends A {
foo = "b";
}
上面示例中,抽象类 A
定义了抽象属性 foo
,子类 B
必须实现这个属性,否则会报错。
下面是抽象方法的例子。如果抽象类的方法前面加上 abstract
,就表明子类必须给出该方法的实现。
abstract class A {
abstract execute(): string;
}
class B extends A {
execute() {
return `B executed`;
}
}
这里有几个注意点。
(1)抽象成员只能存在于抽象类,不能存在于普通类。
(2)抽象成员不能有具体实现的代码。也就是说,已经实现好的成员前面不能加 abstract
关键字。
(3)抽象成员前也不能有 private
修饰符,否则无法在子类中实现该成员。
(4)一个子类最多只能继承一个抽象类。
总之,抽象类的作用是,确保各种相关的子类都拥有跟基类相同的接口,可以看作是模板。其中的抽象成员都是必须由子类实现的成员,非抽象成员则表示基类已经实现的、由所有子类共享的成员。
九、泛型类
类也可以写成泛型,使用类型参数。关于泛型的详细介绍,请看《泛型》一章。
class Box<Type> {
contents: Type;
constructor(value: Type) {
this.contents = value;
}
}
const b: Box<string> = new Box("hello!");
上面示例中,类 Box
有类型参数 Type
,因此属于泛型类。新建实例时,变量的类型声明需要带有类型参数的值,不过本例等号左边的 Box<string>
可以省略不写,因为可以从等号右边推断得到。
注意,静态成员不能使用泛型的类型参数。
class Box<Type> {
static defaultContents: Type; // 报错
}
上面示例中,静态属性 defaultContents
的类型写成类型参数 Type
会报错。因为这意味着调用时必须给出类型参数(即写成 Box<string>.defaultContents
),并且类型参数发生变化,这个属性也会跟着变,这并不是好的做法。
十、类的顶层属性
类的顶层属性是指在类的属性,一般定义在类内部的顶部,所以称为顶层属性。
之所以单独拿出来说,是因为早期的时候,TypeScript 对顶层属性的处理方法,与后来的 ES2022 标准不一致,这可能会导致某些代码看起来逻辑一致,实际上运行结果不一样。
类的顶层属性在 TypeScript 里面,有两种写法。
class User {
// 写法一
age = 25;
// 写法二
constructor(private currentYear: number) {}
}
上面示例中,写法一是直接声明一个实例属性 age
,并初始化;写法二是顶层属性的简写形式,直接将构造方法的参数 currentYear
声明为实例属性。
1、TypeScript 早期处理方法
TypeScript 早期的处理方法是,先在顶层声明属性,但不进行初始化,等到运行构造方法时,再完成所有初始化。
class User {
age = 25;
}
// TypeScript 的早期处理方法
class User {
age: number;
constructor() {
this.age = 25;
}
}
上面示例中,TypeScript 早期会先声明顶层属性 age
,然后等到运行构造函数时,再将其初始化为 25
。
2、ES2022 标准的处理方法
ES2022 标准里面的处理方法是,先进行顶层属性的初始化,再运行构造方法。
3、二者的矛盾点
两种处理方法的区别,会使得同一段代码在 TypeScript 和 JavaScript 下运行结果不一致。这种不一致一般发生在两种情况:
1. 场景 1
第一种情况是,顶层属性的初始化依赖于其他实例属性。
class User {
age = this.currentYear - 1998;
constructor(private currentYear: number) {
// 输出结果将不一致
console.log("Current age:", this.age);
}
}
const user = new User(2023);
按照 TypeScript 的处理方法,初始化是在构造方法里面完成的,会输出结果为 25
。但是,按照 ES2022 标准的处理方法,初始化在声明顶层属性时就会完成,这时 this.currentYear
还等于 undefined
,所以 age
的初始化结果为 NaN
,因此最后输出的也是 NaN
。
2. 场景 2
第二种情况发生在类的继承过程中,子类声明的顶层属性在父类完成初始化。
interface Animal {
animalStuff: any;
}
interface Dog extends Animal {
dogStuff: any;
}
class AnimalHouse {
resident: Animal;
constructor(animal: Animal) {
this.resident = animal;
}
}
class DogHouse extends AnimalHouse {
resident: Dog;
constructor(dog: Dog) {
super(dog);
}
}
上面示例中,类 DogHouse
继承自 AnimalHouse
。它声明了顶层属性 resident
,但是该属性的初始化是在父类 AnimalHouse
完成的。不同的设置运行下面的代码,结果将不一致。
const dog = {
animalStuff: "animal",
dogStuff: "dog",
};
const dogHouse = new DogHouse(dog);
console.log(dogHouse.resident); // 输出结果将不一致
上面示例中,TypeScript 的处理方法,会使得 resident
属性能够初始化,所以输出参数对象的值。但是,ES2022 标准的处理方法是,顶层属性的初始化先于构造方法的运行。这使得 resident
属性不会得到赋值,因此输出为 undefined
。
为了解决这个问题,同时保证以前代码的行为一致,TypeScript 从 3.7 版开始,引入了编译设置 useDefineForClassFields
。这个设置设为 true
,则采用 ES2022 标准的处理方法,否则采用 TypeScript 早期的处理方法。
它的默认值与 target
属性有关,如果输出目标设为 ES2022
或者更高,那么 useDefineForClassFields
的默认值为 true
,否则为 false
。关于这个设置的详细说明,参见官方 3.7 版本的发布说明。
4、如何避免不一致
对于顶层属性初始化依赖的问题
如果希望避免这种不一致,让代码在不同设置下的行为都一样,那么可以将所有顶层属性的初始化,都放到构造方法里面。
class User {
age: number;
constructor(private currentYear: number) {
this.age = this.currentYear - 1998;
console.log("Current age:", this.age);
}
}
const user = new User(2023);
上面示例中,顶层属性 age
的初始化就放在构造方法里面,那么任何情况下,代码行为都是一致的。
对于类的继承场景 declare
对于类的继承,还有另一种解决方法,就是使用 declare
命令,去声明子类顶层属性的类型,告诉 TypeScript 这些属性的初始化由父类实现。
class DogHouse extends AnimalHouse {
declare resident: Dog;
constructor(dog: Dog) {
super(dog);
}
}
上面示例中,resident
属性的类型声明前面用了 declare
命令。这种情况下,这一行代码在编译成 JavaScript 后就不存在,那么也就不会有行为不一致,无论是否设置 useDefineForClassFields
,输出结果都是一样的。
十一、装饰器
装饰器(Decorator)是一种语法结构。一种声明式的方式来添加元数据或修改类和类成员的行为,使得开发者能够在类声明时或类成员定义时添加额外的功能,而不必修改原始实现。
具体用法,请阅读 《Decorator 装饰器》 章节,本文不再做详细介绍
装饰器的形式如下:
@decorator
class MyClass {
@decorator
method() {
// ...
}
}
装饰器主要分为以下几种:
- 类装饰器:用来装饰类的定义。
- 属性装饰器:用来装饰类的属性。
- 方法装饰器:用来装饰类的方法。
getter
、setter
装饰器:用来装饰类的getter
和setter
方法。- 访问器装饰器:用来装饰类的访问器。
十二、this
指向问题
类的方法经常用到 this
关键字,它表示该方法当前所在的对象。
class A {
name = "A";
getName() {
return this.name;
}
}
const a = new A();
a.getName(); // 'A'
const b = {
name: "b",
getName: a.getName,
};
b.getName(); // 'b'
上面示例中,变量 a
和 b
的 getName()
是同一个方法,但是执行结果不一样,原因就是它们内部的 this
指向不一样的对象。如果 getName()
在变量 a
上运行,this
指向 a
;如果在 b
上运行,this
指向 b
。
有些场合需要给出 this
类型,但是 JavaScript 函数通常不带有 this
参数,这时 TypeScript 允许函数增加一个名为 this
的参数,放在参数列表的第一位,用来描述函数内部的 this
关键字的类型。
// 编译前
function fn(this: SomeType, x: number) {
/* ... */
}
// 编译后
function fn(x) {
/* ... */
}
上面示例中,函数 fn()
的第一个参数是 this
,用来声明函数内部的 this
的类型。编译时,TypeScript 一旦发现函数的第一个参数名为 this
,则会去除这个参数,即编译结果不会带有该参数。
class A {
name = "A";
getName(this: A) {
return this.name;
}
}
const a = new A();
const b = a.getName;
b(); // 报错
上面示例中,类 A
的 getName()
添加了 this
参数,如果直接调用这个方法,this
的类型就会跟声明的类型不一致,从而报错。
this
参数的类型可以声明为各种对象。
function foo(this: { name: string }) {
this.name = "Jack";
this.name = 0; // 报错
}
foo.call({ name: 123 }); // 报错
上面示例中,参数 this
的类型是一个带有 name
属性的对象,不符合这个条件的 this
都会报错。
TypeScript 提供了一个 noImplicitThis
编译选项。如果打开了这个设置项,如果 this
的值推断为 any
类型,就会报错。
// noImplicitThis 打开
class Rectangle {
constructor(public width: number, public height: number) {}
getAreaFunction() {
return function () {
return this.width * this.height; // 报错
};
}
}
上面示例中,getAreaFunction()
方法返回一个函数,这个函数里面用到了 this
,但是这个 this
跟 Rectangle
这个类没关系,它的类型推断为 any
,所以就报错了。
在类的内部,this
本身也可以当作类型使用,表示当前类的实例对象。
class Box {
contents: string = "";
set(value: string): this {
this.contents = value;
return this;
}
}
上面示例中,set()
方法的返回值类型就是 this
,表示当前的实例对象。
注意,this
类型不允许应用于静态成员。
class A {
static a: this; // 报错
}
上面示例中,静态属性a
的返回值类型是 this
,就报错了。原因是 this
类型表示实例对象,但是静态成员拿不到实例对象。
有些方法返回一个布尔值,表示当前的this
是否属于某种类型。这时,这些方法的返回值类型可以写成 this is Type
的形式,其中用到了is
运算符。
class FileSystemObject {
isFile(): this is FileRep {
return this instanceof FileRep;
}
isDirectory(): this is Directory {
return this instanceof Directory;
}
// ...
}
上面示例中,两个方法的返回值类型都是布尔值,写成 this is Type
的形式,可以精确表示返回值。is
运算符的介绍详见《类型断言》一章。
更新日志
e7112
-1于