跳到主要内容

TypeScript 类

TypeScript 完全支持 ES2015 引入的 class 关键字,并在此基础上添加了类型注解和其他语法,用于表达类与其他类型之间的关系。

类成员

字段(Fields)

字段声明会在类上创建一个公共的可写属性:

class Point {
x: number;
y: number;
}

const point = new Point();
point.x = 10;
point.y = 20;

console.log(`坐标: (${point.x}, ${point.y})`); // 坐标: (10, 20)

解释xy 是类的字段,类型注解是可选的。如果未指定类型,则默认为 any 类型。

字段可以有初始值,在类实例化时会自动执行:

class Point {
x = 0;
y = 0;
}

const point = new Point();
console.log(`${point.x}, ${point.y}`); // 0, 0

解释:初始值会用于推断字段类型,xy 被推断为 number 类型。

readonly 修饰符

使用 readonly 修饰符可以将字段设为只读,只能在构造函数中赋值:

class Greeter {
readonly name: string = "world";

constructor(otherName?: string) {
if (otherName !== undefined) {
this.name = otherName;
}
}

err() {
// 错误:无法分配到 "name" ,因为它是只读属性
// this.name = "not ok";
}
}

const g = new Greeter("hello");
// g.name = "also not ok"; // 错误:无法分配到 "name"
console.log(g.name); // "hello"

解释readonly 属性只能在声明时或构造函数中初始化,之后无法修改。这有助于创建不可变对象。

构造函数(Constructors)

构造函数与函数类似,可以添加参数类型注解、默认值和重载:

class Point {
x: number;
y: number;

// 带默认值的构造函数
constructor(x: number = 0, y: number = 0) {
this.x = x;
this.y = y;
}
}

const p1 = new Point(); // (0, 0)
const p2 = new Point(10); // (10, 0)
const p3 = new Point(10, 20); // (10, 20)

构造函数重载

class Point {
x: number;
y: number;

constructor(x: number, y: number);
constructor(xy: string);
constructor(xOrXy: number | string, y?: number) {
if (typeof xOrXy === "string") {
// 解析字符串格式的坐标
const parts = xOrXy.split(",");
this.x = parseFloat(parts[0]);
this.y = parseFloat(parts[1]);
} else {
this.x = xOrXy;
this.y = y ?? 0;
}
}
}

const p1 = new Point(10, 20); // 数字坐标
const p2 = new Point("10,20"); // 字符串坐标

解释:构造函数重载允许类以多种方式实例化,实现签名必须兼容所有重载签名。

super 调用

如果类继承自基类,在使用 this 之前必须调用 super()

class Base {
k = 4;
}

class Derived extends Base {
constructor() {
// 错误:在派生类的构造函数中访问 'this' 前必须调用 'super()'
// console.log(this.k);

super(); // 必须先调用 super()
console.log(this.k); // 4
}
}

解释:JavaScript 要求在派生类构造函数中,必须先完成父类构造函数的调用(super())才能访问 this。TypeScript 会检查这一点。

方法(Methods)

类中的函数属性称为方法,可以使用与函数相同的类型注解:

class Point {
x = 10;
y = 10;

scale(n: number): void {
this.x *= n;
this.y *= n;
}
}

const point = new Point();
point.scale(2);
console.log(`${point.x}, ${point.y}`); // 20, 20

重要提示:在方法体内,访问字段和其他方法必须通过 this.

let x = 10;  // 外部变量

class Example {
x = "hello";

method() {
// 这是修改外部的 x,不是类的属性
// x = "world"; // 类型错误:string 不能赋给 number

// 正确的方式
this.x = "world";
}
}

存取器(Getters / Setters)

类可以有存取器(getter 和 setter):

class Circle {
private _radius: number = 0;

get radius(): number {
return this._radius;
}

set radius(value: number) {
if (value < 0) {
throw new Error("半径不能为负数");
}
this._radius = value;
}

get area(): number {
return Math.PI * this._radius ** 2;
}
}

const circle = new Circle();
circle.radius = 10;
console.log(circle.radius); // 10
console.log(circle.area); // 314.159...

// circle.area = 100; // 错误:area 只有 getter,是只读的

解释

  • 只有 get 没有 set 的属性自动变为 readonly
  • 如果未指定 setter 参数类型,会从 getter 的返回类型推断

从 TypeScript 4.3 开始,getter 和 setter 可以使用不同的类型:

class Thing {
private _size = 0;

get size(): number {
return this._size;
}

set size(value: string | number | boolean) {
const num = Number(value);
if (!Number.isFinite(num)) {
this._size = 0;
return;
}
this._size = num;
}
}

const thing = new Thing();
thing.size = 100; // number
thing.size = "200"; // string
thing.size = true; // boolean (转为 1)

继承

extends 关键字

类可以继承自基类,派生类拥有基类的所有属性和方法:

class Animal {
move(): void {
console.log("正在移动...");
}
}

class Dog extends Animal {
woof(times: number): void {
for (let i = 0; i < times; i++) {
console.log("汪!");
}
}
}

const dog = new Dog();
dog.move(); // 继承自基类的方法
dog.woof(3); // 派生类自己的方法

方法重写

派生类可以重写基类的方法,使用 super. 语法访问基类方法:

class Base {
greet(): void {
console.log("Hello, world!");
}
}

class Derived extends Base {
greet(name?: string): void {
if (name === undefined) {
super.greet(); // 调用基类方法
} else {
console.log(`Hello, ${name.toUpperCase()}`);
}
}
}

const d = new Derived();
d.greet(); // Hello, world!
d.greet("reader"); // Hello, READER

重要:派生类必须遵循基类的约定。重写方法时,参数类型必须兼容:

class Base {
greet(): void {
console.log("Hello!");
}
}

class Derived extends Base {
// 错误:派生类方法参数不兼容
// greet(name: string): void {
// console.log(`Hello, ${name}`);
// }
}

解释:因为可以这样调用 const b: Base = d; b.greet();,如果 greet 需要参数,就会出错。

初始化顺序

JavaScript 类的初始化顺序可能令人惊讶:

class Base {
name = "base";

constructor() {
console.log("My name is " + this.name);
}
}

class Derived extends Base {
name = "derived";
}

const d = new Derived(); // 输出 "base",不是 "derived"!

解释:初始化顺序如下:

  1. 基类字段初始化(name = "base"
  2. 基类构造函数执行(输出 this.name,此时是 "base")
  3. 派生类字段初始化(name = "derived"
  4. 派生类构造函数执行

所以在基类构造函数执行时,派生类的字段还未初始化。

implements 子句

使用 implements 子句检查类是否满足特定接口:

interface Pingable {
ping(): void;
}

class Sonar implements Pingable {
ping(): void {
console.log("ping!");
}
}

// 错误:类 'Ball' 错误实现接口 'Pingable'
// class Ball implements Pingable {
// pong(): void {
// console.log("pong!");
// }
// }

类可以实现多个接口:

interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}

interface AlarmInterface {
alarm(): void;
}

class Clock implements ClockInterface, AlarmInterface {
currentTime: Date = new Date();

setTime(d: Date): void {
this.currentTime = d;
}

alarm(): void {
console.log("闹钟响了!");
}
}

注意implements 子句只是检查类是否符合接口,不会改变类的类型:

interface Checkable {
check(name: string): boolean;
}

class NameChecker implements Checkable {
// 注意:参数 s 没有 string 类型!
check(s) { // s 隐式为 any 类型
return s.toLowerCase() === "ok";
}
}

解释implements 不会自动给方法参数添加类型,需要手动添加。

成员可见性

TypeScript 提供三种访问修饰符控制成员可见性。

public(公开)

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

class Greeter {
public greet(): void {
console.log("hi!");
}
}

const g = new Greeter();
g.greet(); // OK

public 是默认的,所以不需要显式写出来。

protected(受保护)

protected 成员只能在声明它的类及其子类中访问:

class Greeter {
public greet(): void {
console.log("Hello, " + this.getName());
}

protected getName(): string {
return "hi";
}
}

class SpecialGreeter extends Greeter {
public howdy(): void {
// 可以在子类中访问 protected 成员
console.log("Howdy, " + this.getName());
}
}

const g = new SpecialGreeter();
g.greet(); // OK
// g.getName(); // 错误:属性 'getName' 受保护

派生类可以将 protected 成员暴露为 public

class Base {
protected m = 10;
}

class Derived extends Base {
// 没有修饰符,默认是 'public'
m = 15;
}

const d = new Derived();
console.log(d.m); // OK,m 现在是 public

private(私有)

private 成员只能在声明它的类中访问,子类也无法访问:

class Base {
private x = 0;
}

const b = new Base();
// console.log(b.x); // 错误:属性 'x' 是私有的

class Derived extends Base {
showX() {
// 错误:属性 'x' 是私有的
// console.log(this.x);
}
}

跨实例私有访问:TypeScript 允许同一类的不同实例互相访问私有成员:

class A {
private x = 10;

public sameAs(other: A): boolean {
// 可以访问其他实例的私有成员
return other.x === this.x;
}
}

const a1 = new A();
const a2 = new A();
console.log(a1.sameAs(a2)); // true

JavaScript 私有字段(#):ES2022 引入了真正的私有字段语法:

class Dog {
#barkAmount = 0; // 真正的私有字段
personality = "happy";

bark(): void {
this.#barkAmount++;
console.log(`汪!(${this.#barkAmount} 次)`);
}
}

const dog = new Dog();
dog.bark(); // 汪!(1 次)
// dog.#barkAmount; // 语法错误

解释# 前缀的私有字段是 JavaScript 原生的私有字段,在运行时也保持私有,而 TypeScript 的 private 只是编译时检查。

静态成员

静态成员不属于某个特定实例,而是属于类本身:

class MyClass {
static x = 0;
static printX(): void {
console.log(MyClass.x);
}
}

console.log(MyClass.x); // 0
MyClass.printX(); // 0

const instance = new MyClass();
// instance.x; // 错误:x 是静态成员

静态成员也可以使用可见性修饰符:

class MyClass {
private static x = 0;

public static getX(): number {
return MyClass.x;
}
}

// console.log(MyClass.x); // 错误:x 是私有的
console.log(MyClass.getX()); // 0

静态成员也会被继承:

class Base {
static getGreeting(): string {
return "Hello world";
}
}

class Derived extends Base {
myGreeting = Derived.getGreeting(); // 继承静态方法
}

静态代码块

静态代码块用于初始化静态成员:

class Foo {
static #count = 0;

get count(): number {
return Foo.#count;
}

static {
// 静态初始化块
try {
const lastInstances = loadLastInstances();
Foo.#count += lastInstances.length;
} catch {
// 忽略错误
}
}
}

抽象类

抽象类不能被实例化,只能被继承:

abstract class Animal {
// 抽象方法:没有实现,必须由子类实现
abstract makeSound(): void;

// 具体方法:有实现,子类可以直接继承
move(): void {
console.log("正在移动...");
}
}

// const animal = new Animal(); // 错误:无法创建抽象类的实例

class Dog extends Animal {
makeSound(): void {
console.log("汪汪汪!");
}
}

const dog = new Dog();
dog.makeSound(); // 汪汪汪!
dog.move(); // 正在移动...

解释

  • abstract 关键字标记抽象类和抽象方法
  • 抽象方法必须在派生类中实现
  • 抽象类可以包含具体实现的方法

泛型类

类可以有泛型参数:

class Box<T> {
contents: T;

constructor(value: T) {
this.contents = value;
}

getValue(): T {
return this.contents;
}
}

const stringBox = new Box<string>("hello");
console.log(stringBox.getValue()); // "hello"

const numberBox = new Box(123); // 类型推断为 Box<number>
console.log(numberBox.getValue()); // 123

泛型类可以使用泛型约束和默认值:

interface Item {
id: number;
}

class Collection<T extends Item> {
private items: T[] = [];

add(item: T): void {
this.items.push(item);
}

findById(id: number): T | undefined {
return this.items.find(item => item.id === id);
}
}

interface User extends Item {
id: number;
name: string;
}

const users = new Collection<User>();
users.add({ id: 1, name: "张三" });
users.add({ id: 2, name: "李四" });

console.log(users.findById(1)); // { id: 1, name: "张三" }

注意:静态成员不能引用类的泛型参数:

class Box<T> {
// 错误:静态成员不能引用类类型参数
// static defaultValue: T;
}

解释:因为运行时只有一个 Box.defaultValue,而 Box<string>Box<number> 是不同的类型。

this 类型

在类中,特殊的 this 类型动态指向当前类的类型:

class Box {
content: string = "";

set(value: string): this {
this.content = value;
return this; // 返回 this 类型
}
}

class ClearableBox extends Box {
clear(): this {
this.content = "";
return this;
}
}

const box = new ClearableBox();
const result = box.set("hello"); // result 类型是 ClearableBox,不是 Box
result.clear(); // OK

解释:使用 this 作为返回类型,可以方便地实现链式调用,并且在继承时保持正确的类型。

this 也可以用作参数类型:

class Box {
content: string = "";

sameAs(other: this): boolean {
return other.content === this.content;
}
}

class DerivedBox extends Box {
extra: string = "";
}

const base = new Box();
const derived = new DerivedBox();

derived.sameAs(base); // OK
// base.sameAs(derived); // 错误:参数类型不匹配

this 类型守卫

可以在方法返回类型中使用 this is Type

class FileSystemObject {
isFile(): this is FileRep {
return this instanceof FileRep;
}

isDirectory(): this is Directory {
return this instanceof Directory;
}
}

class FileRep extends FileSystemObject {
constructor(public path: string, public content: string) {
super();
}
}

class Directory extends FileSystemObject {
children: FileSystemObject[] = [];
}

function process(fso: FileSystemObject) {
if (fso.isFile()) {
// fso 类型被收窄为 FileRep
console.log(fso.content);
} else if (fso.isDirectory()) {
// fso 类型被收窄为 Directory
console.log(fso.children);
}
}

参数属性

TypeScript 提供了一种简写方式,将构造函数参数自动变成类属性:

class Person {
// 传统写法
// name: string;
// age: number;
// constructor(name: string, age: number) {
// this.name = name;
// this.age = age;
// }

// 参数属性简写
constructor(
public name: string,
public age: number,
private id: number,
readonly email: string
) {}
}

const person = new Person("张三", 25, 1, "[email protected]");
console.log(person.name); // 张三
console.log(person.age); // 25
console.log(person.email); // [email protected]
// console.log(person.id); // 错误:id 是私有的

解释:在构造函数参数前添加可见性修饰符(publicprivateprotected)或 readonly,TypeScript 会自动创建并初始化对应的属性。

小结

本章我们学习了 TypeScript 类的核心概念:

  1. 类成员:字段、方法、构造函数、存取器
  2. 继承extends 和方法重写
  3. 接口实现implements 子句
  4. 成员可见性publicprotectedprivate
  5. 静态成员:属于类本身的成员
  6. 抽象类:不能实例化的基类
  7. 泛型类:带类型参数的类
  8. this 类型:动态指向当前类类型
  9. 参数属性:构造函数参数的简写语法

练习

  1. 创建一个 Rectangle 类,包含宽度和高度属性,以及计算面积和周长的方法
  2. 创建一个抽象类 Shape,派生出 CircleSquare
  3. 实现一个泛型类 Stack<T>,包含 push、pop 和 peek 方法
  4. 使用参数属性简化一个包含多个属性的类