【阮一峰】8.类
- 创业
- 2025-08-30 05:15:01

类 简介 属性的类型
类的属性可以在顶层声明,也可以在构造方法内部声明。
对于顶层声明的属性,可以在声明时同时给出类型。
class Point { x: number; y: number; }如果不给出类型,TypeScript 会认为 x 和 y 的类型都是 any。
class Point { x; y; }如果声明时给出初值,可以不写类型,TypeScript 会自行推断属性的类型。
class Point { x = 0; y = 0; } readonly 修饰符属性名前面加上 readonly 修饰符,就表示该属性是只读的。实例对象不能修改这个属性。
class A { readonly id = "foo"; } const a = new A(); a.id = "bar"; // 报错readonly 属性的初始值,可以写在顶层属性,也可以写在构造方法里面。
class A { readonly id: string; constructor() { this.id = "bar"; // 正确 } } 方法的类型类的方法就是普通函数,类型声明方式与函数一致。可以使用参数默认值,以及函数重载。
class Point { constructor(x: number, y: string); constructor(s: string); constructor(xs: number | string, y?: string) { // ... } }:::tip 构造方法不能声明返回值类型,否则报错,因为它总是返回实例对象。 :::
存取器方法存取器(accessor)是特殊的类方法,包括取值器(getter)和存值器(setter)两种方法。
TypeScript 对存取器有以下规则。
(1)如果某个属性只有 get 方法,没有 set 方法,那么该属性自动成为只读属性。
class C { _name = "foo"; get name() { return this._name; } } const c = new C(); c.name = "bar"; // 报错(2)get 方法与 set 方法的可访问性必须一致,要么都为公开方法,要么都为私有方法。
属性索引类允许定义属性索引。
class MyClass { [s: string]: boolean | ((s: string) => boolean); get(s: string) { return this[s] as boolean; } }:::tip 由于类的方法是一种特殊属性(属性值为函数的属性),所以属性索引的类型定义也涵盖了方法。如果一个对象同时定义了属性索引和方法,那么前者必须包含后者的类型。 :::
class MyClass { [s: string]: boolean; f() { // 报错 return true; } }属性存取器视同属性。
class MyClass { [s: string]: boolean; get isInstance() { return true; } } 类的 interface 接口 implements 关键字interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制。
interface Country { name: string; capital: string; } // 或者 type Country = { name: string; capital: string; }; class MyCountry implements Country { name = ""; capital = ""; }interface 只是指定检查条件,如果不满足这些条件就会报错。它并不能代替 class 自身的类型声明。
interface A { get(name: string): boolean; } class B implements A { get(s) { // s 的类型是 any,这里还需要声明s的类型string return true; } }类可以定义接口没有声明的方法和属性。
interface Point { x: number; y: number; } class MyPoint implements Point { x = 1; y = 1; z: number = 1; }implements 关键字后面,不仅可以是接口,也可以是另一个类。这时,后面的类将被当作接口。
class Car { id: number = 1; move(): void {} } class MyCar implements Car { id = 2; // 不可省略 move(): void {} // 不可省略 }interface 描述的是类的对外接口,也就是实例的公开属性和公开方法,不能定义私有的属性和方法。这是因为 TypeScript 设计者认为,私有属性是类的内部实现,接口作为模板,不应该涉及类的内部代码写法。
interface Foo { member: {}; // 报错 } 实现多个接口类可以实现多个接口(其实是接受多重限制),每个接口之间使用逗号分隔。
class Car implements MotorVehicle, Flyable, Swimmable { // ... }但是,同时实现多个接口并不是一个好的写法,容易使得代码难以管理,可以使用两种方法替代。
第一种方法是类的继承。
class Car implements MotorVehicle {} class SecretCar extends Car implements Flyable, Swimmable {}类 Car 实现了接口 MotorVehicle,而 SecretCar 继承了 Car,也实现了 Flyable 和 Swimmable 接口。相当于 SecretCar 同时实现了多个接口。
第二种方法是接口的继承。
interface A { a: number; } interface B extends A { b: number; } interface MotorVehicle { // ... } interface Flyable { // ... } interface Swimmable { // ... } interface SuperCar extends MotorVehicle, Flyable, Swimmable { // ... } class SecretCar implements SuperCar { // ... }:::tip 发生多重实现时(即一个接口同时实现多个接口),不同接口不能有互相冲突的属性。 :::
类与接口的合并TypeScript 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类
class A { x: number = 1; } interface A { y: number; } let a = new A(); a.y = 10; a.x; // 1 a.y; // 10合并进类的非空属性(上例的 y),如果在赋值之前读取,会返回 undefined。
class A { x: number = 1; } interface A { y: number; } let a = new A(); a.y; // undefined Class 类型 实例类型TypeScript 的类本身就是一种类型,但是它代表该类的实例类型,而不是 class 的自身类型。
class Color { name: string; constructor(name: string) { this.name = name; } } const green: Color = new Color("green");由于类名作为类型使用,实际上代表一个对象,因此可以把类看作对象类型的起名。事实上,TypeScript 有三种方法可以为对象类型起名:type、interface 和 class。
类的自身类型要获得一个类的自身类型,一个简便的方法就是使用 typeof 运算符。
class Point { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } } function createPoint(PointClass: typeof Point, x: number, y: number): Point { return new PointClass(x, y); }类的自身类型可以写成构造函数的形式。
function createPoint( PointClass: new (x: number, y: number) => Point, x: number, y: number ): Point { return new PointClass(x, y); }构造函数也可以写成对象形式。
function createPoint( PointClass: { new (x: number, y: number): Point; }, 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); // 正确这种情况,运算符 instanceof 不适用于判断某个对象是否跟某个 class 属于同一类型。
如果两个类的实例结构相同,那么这两个类就是兼容的,可以用在对方的使用场合。
class Person { name: string; } class Customer { name: string; } // 正确 const cust: Customer = new Person();总之,只要 A 类具有 B 类的结构,哪怕还有额外的属性和方法,TypeScript 也认为 A 兼容 B 的类型。
:::tip 此时无法通过 instanceof 判断某个对象是否跟某个 class 属于同一类型。 :::
空类不包含任何成员,任何其他类都可以看作与空类结构相同。因此,凡是类型为空类的地方,所有类(包括对象)都可以使用。
class Empty {} function fn(x: Empty) { // ... } fn({}); fn(window); fn(fn);:::tip 确定两个类的兼容关系时,只检查实例成员,不考虑静态成员和构造方法。 :::
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("");如果类中存在私有成员(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。
类的继承类可以使用 extends 关键字继承另一个类的所有属性和方法。
根据结构类型原则,子类也可以用于类型为基类的场合。
子类可以覆盖基类的同名方法。
class A { greet() { console.log("Hello, world!"); } } class B extends A { greet(name?: string) { if (name === undefined) { super.greet(); } else { console.log(`Hello, ${name}`); } } }但是,子类的同名方法不能与基类的类型定义相冲突。
class A { greet() { console.log("Hello, world!"); } } class B extends A { // 报错 greet(name: string) { console.log(`Hello, ${name}`); } }如果基类包括保护成员(protected 修饰符),子类可以将该成员的可访问性设置为公开(public 修饰符),也可以保持保护成员不变,但是不能改用私有成员(private 修饰符)。
class A { protected x: string = ""; protected y: string = ""; protected z: string = ""; } class B extends A { // 正确 public x: string = ""; // 正确 protected y: string = ""; // 报错 private z: string = ""; }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()); } } override 关键字子类继承父类时,可以覆盖父类的同名方法。防止在继承他人的类时,会在不知不觉中就覆盖了他人的方法,TypeScript 4.3 引入了 override 关键字。
class A { show() { // ... } hide() { // ... } } class B extends A { override show() { // ... } override hide() { // ... } } 可访问性修饰符类的内部成员的外部可访问性,由三个可访问性修饰符(access modifiers)控制:public、private 和 protected。
publicpublic 修饰符表示这是公开成员,外部可以自由访问。
class Greeter { public greet() { console.log("hi!"); } } const g = new Greeter(); g.greet();public 修饰符是默认修饰符,通常省略不写。
privateprivate 修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员。
class A { private x: number = 0; } const a = new A(); a.x; // 报错 class B extends A { showX() { console.log(this.x); // 报错 } }:::tip 子类不能定义父类私有成员的同名成员。 :::
class A { private x = 0; } class B extends A { x = 1; // 报错 }如果在类的内部,当前类的实例可以获取私有成员。
class A { private x = 10; f(obj: A) { console.log(obj.x); } } const a = new A(); a.f(a); // 10严格地说,private 定义的私有成员,并不是真正意义的私有成员。
原因一:编译成 JavaScript 后,private 关键字就被剥离了,这时外部访问该成员就不会报错。
原因二:TypeScript 对于访问 private 成员没有严格禁止,使用方括号写法([])或者 in 运算符,实例对象就能访问该成员。
class A { private x = 1; } const a = new A(); a["x"]; // 1 if ("x" in a) { // 正确 // ... }ES2022 引入了自己的私有成员写法#propName,解决了这些弊端。
class A { #x = 1; } const a = new A(); a["x"]; // 报错 protectedprotected 修饰符表示该成员是保护成员,只能在类的内部和子类内部使用该成员,实例无法使用该成员。
class A { protected x = 1; } class B extends A { getX() { return this.x; } } const a = new A(); const b = new B(); a.x; // 报错 b.getX(); // 1子类不仅可以拿到父类的保护成员,还可以定义同名成员。
class A { protected x = 1; } class B extends A { x = 2; } 实例属性的简写形式 class Point { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } }等同于
class Point { constructor(public x: number, public y: number) {} } const p = new Point(10, 10); p.x; // 10 p.y; // 10除了 public 修饰符,构造方法的参数名只要有 private、protected、readonly 修饰符,都会自动声明对应修饰符的实例属性。
class A { constructor( public a: number, protected b: number, private c: number, readonly d: number ) {} } // 编译结果 class A { a; b; c; d; constructor(a, b, c, d) { this.a = a; this.b = b; this.c = c; this.d = d; } }readonly 还可以与其他三个可访问性修饰符,一起使用。
class A { constructor( public readonly x: number, protected readonly y: number, private readonly z: number ) {} } 静态成员类的内部可以使用 static 关键字,定义静态成员。
静态成员是只能通过类本身使用的成员,不能通过实例对象使用。
class MyClass { static x = 0; static printX() { console.log(MyClass.x); } } MyClass.x; // 0 MyClass.printX(); // 0static 关键字前面可以使用 public、private、protected 修饰符。
静态私有属性也可以用 ES6 语法的#前缀表示
class MyClass { static #x = 0; }public 和 protected 的静态成员可以被继承。
class A { public static x = 1; protected static y = 1; } class B extends A { static getY() { return B.y; } } B.x; // 1 B.getY(); // 1 泛型类类也可以写成泛型,使用类型参数。
class Box<Type> { contents: Type; constructor(value: Type) { this.contents = value; } } const b: Box<string> = new Box("hello!");:::tip 静态成员不能使用泛型的类型参数。 :::
class Box<Type> { static defaultContents: Type; // 报错 } 抽象类,抽象成员在类的定义前面,加上关键字 abstract,表示该类不能被实例化,只能当作其他类的模板。
抽象类只能当作基类使用,用来在它的基础上定义子类。
abstract class A { id = 1; } class B extends A { amount = 100; } const b = new B(); b.id; // 1 b.amount; // 100抽象类的子类也可以是抽象类,也就是说,抽象类可以继承其他抽象类。
abstract class A { foo: number; } abstract class B extends A { bar: string; }抽象类的内部可以有已经实现好的属性和方法,也可以有还未实现的属性和方法。后者就叫做“抽象成员”(abstract member),即属性名和方法名有 abstract 关键字,表示该方法需要子类实现。如果子类没有实现抽象成员,就会报错。
abstract class A { abstract foo: string; bar: string = ""; abstract execute(): string; } class B extends A { foo = "b"; execute() { return `B executed`; } }这里有几个注意点。
(1)抽象成员只能存在于抽象类,不能存在于普通类。
(2)抽象成员不能有具体实现的代码。也就是说,已经实现好的成员前面不能加 abstract 关键字。
(3)抽象成员前也不能有 private 修饰符,否则无法在子类中实现该成员。
(4)一个子类最多只能继承一个抽象类。
this 问题类的方法经常用到 this 关键字,它表示该方法当前所在的对象。
this 参数的类型可以声明为各种对象。
function foo(this: { name: string }) { this.name = "Jack"; this.name = 0; // 报错 } foo.call({ name: 123 }); // 报错在类的内部,this 本身也可以当作类型使用,表示当前类的实例对象。
class Box { contents: string = ""; set(value: string): this { this.contents = value; return this; } }:::tip this 类型不允许应用于静态成员。 :::
class A { static a: this; // 报错 }有些方法返回一个布尔值,表示当前的 this 是否属于某种类型。这时,这些方法的返回值类型可以写成 this is Type 的形式,其中用到了 is 运算符。
class FileSystemObject { isFile(): this is FileRep { return this instanceof FileRep; } isDirectory(): this is Directory { return this instanceof Directory; } // ... }