📚 TypeScript 教程系列

  1. 入门与配置
  2. 基础类型与变量声明
  3. 函数
  4. 流程控制与运算符
  5. 集合类型
  6. 异步编程与错误处理
  7. 接口与类(本文)
  8. 泛型与类型组合
  9. 高级类型
  10. 模块、装饰器与工程化

⚠️ 来源声明:本文内容参考自 菜鸟教程 TypeScript 教程,仅供学习交流,版权归原作者所有。
TypeScript 的接口与类是面向对象编程的核心构件:接口负责描述对象的形状与契约,类负责封装数据与行为并支持继承与多态。本篇将接口、类、访问修饰符、抽象类、继承与混入串联起来,形成一条从"类型契约"到"代码复用"的完整脉络。

接口

接口是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的类去实现,然后第三方就可以通过这组抽象方法调用,让具体的类执行具体的方法。需要注意接口不会转换为 JavaScript,它只是 TypeScript 编译时的一部分。

TypeScript 接口定义如下:

1
2
interface interface_name {
}

接口属性与方法

以下实例中,定义了一个接口 IPerson,接着定义了一个变量 customer,它的类型是 IPersoncustomer 实现了接口的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
interface IPerson {
firstName: string,
lastName: string,
sayHi: () => string
}

var customer: IPerson = {
firstName: "Tom",
lastName: "Hanks",
sayHi: (): string => { return "Hi there" }
}

console.log("Customer 对象 ")
console.log(customer.firstName)
console.log(customer.lastName)
console.log(customer.sayHi())

var employee: IPerson = {
firstName: "Jim",
lastName: "Blakes",
sayHi: (): string => { return "Hello!!!" }
}

console.log("Employee 对象 ")
console.log(employee.firstName)
console.log(employee.lastName)

输出结果为:

1
2
3
4
5
6
7
Customer 对象
Tom
Hanks
Hi there
Employee 对象
Jim
Blakes

可选属性

接口里的属性不全都是必需的。带有可选属性的接口与普通接口类似,只是在属性名后面加一个 ?,表示该属性可以不存在。这对于"条件性"的对象描述非常有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Person {
name: string;
age?: number; // 可选属性
phone?: string; // 可选属性
}

// 不写 age、phone 也合法
var p1: Person = { name: "Alice" };

// 写上同样合法
var p2: Person = { name: "Bob", age: 20, phone: "13800000000" };

console.log(p1.name);
console.log(p2.name + ", " + p2.age);

只读属性

一些对象属性只能在对象刚刚创建时修改其值,使用 readonly 指定只读属性。只读属性只能在声明时或构造函数里赋值,之后不可修改。

1
2
3
4
5
6
7
8
9
10
interface Point {
readonly x: number;
readonly y: number;
}

var p: Point = { x: 10, y: 20 };

console.log(p.x + ", " + p.y);

// p.x = 5; // 错误:只读属性不可修改

readonlyconst 的区别:const 用于变量,readonly 用于属性。若希望属性不可变,使用 readonly;若希望变量名绑定的引用不可变,使用 const

索引签名

接口可以描述"任意数量的属性"的对象,通过索引签名实现。索引值可以是数字或字符串。

设置元素为字符串类型:

1
2
3
4
5
6
7
8
9
interface namelist {
[index: number]: string
}

// 类型一致,正确
var list2: namelist = ["Google", "Runoob", "Taobao"]

// 错误元素 1 不是 string 类型
// var list2: namelist = ["Runoob", 1, "Taobao"]

如果使用了其他类型会报错:

1
2
3
4
5
6
interface namelist {
[index: number]: string
}

// 错误元素 1 不是 string 类型
var list2: namelist = ["John", 1, "Bran"]

执行后报错如下,显示类型不一致:

1
2
3
4
5
6
7
8
9
10
11
12
test.ts:8:30 - error TS2322: Type 'number' is not assignable to type 'string'.

8 var list2:namelist = ["John",1,"Bran"]
~

test.ts:2:4
2 [index:number]:string
~~~~~~~~~~~~~~~~~~~~~
The expected type comes from this index signature.


Found 1 error.

字符串索引签名则允许以任意字符串作为键:

1
2
3
4
5
6
7
8
9
10
11
interface ages {
[index: string]: number
}

var agelist: ages;

// 类型正确
agelist["runoob"] = 15

// 类型错误,输出 error TS2322: Type '"google"' is not assignable to type 'number'.
// agelist[2] = "google"

函数类型接口

接口除了描述带属性的对象,也可以描述函数类型。为函数类型定义接口时,需要给接口定义一个调用签名,就像是一个只有参数列表和返回值类型的函数定义。

1
2
3
4
5
6
7
8
9
10
11
12
interface SearchFunc {
(source: string, subString: string): boolean;
}

var mySearch: SearchFunc;
mySearch = function (src: string, sub: string): boolean {
var result = src.search(sub);
return result > -1;
}

console.log(mySearch("Hello World", "World")); // true
console.log(mySearch("Hello World", "world")); // false

函数的参数名不需要与接口里定义的名字一致,只要对应位置上的类型兼容即可。

联合类型与接口

以下实例演示了如何在接口中使用联合类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface RunOptions {
program: string;
commandline: string[] | string | (() => string);
}

// commandline 是字符串
var options: RunOptions = { program: "test1", commandline: "Hello" };
console.log(options.commandline)

// commandline 是字符串数组
options = { program: "test1", commandline: ["Hello", "World"] };
console.log(options.commandline[0]);
console.log(options.commandline[1]);

// commandline 是一个函数表达式
options = { program: "test1", commandline: () => { return "**Hello World**"; } };
var fn: any = options.commandline;
console.log(fn());

输出结果为:

1
2
3
4
Hello
Hello
World
**Hello World**

接口与数组

接口中可以将数组的索引值和元素设置为不同类型,索引值可以是数字或字符串。数字索引签名常用于约束数组元素类型,字符串索引签名常用于约束字典对象。

1
2
3
4
5
6
interface namelist {
[index: number]: string
}

// 类型一致,正确
var list2: namelist = ["Google", "Runoob", "Taobao"]

接口继承

接口继承就是说接口可以通过其他接口来扩展自己。TypeScript 允许接口继承多个接口,继承使用关键字 extends

单接口继承语法格式:

1
Child_interface_name extends super_interface_name

多接口继承语法格式:

1
Child_interface_name extends super_interface1_name, super_interface2_name, ..., super_interfaceN_name

继承的各个接口使用逗号 , 分隔。

单继承实例

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Person {
age: number
}

interface Musician extends Person {
instrument: string
}

var drummer = <Musician>{};
drummer.age = 27
drummer.instrument = "Drums"
console.log("年龄: " + drummer.age)
console.log("喜欢的乐器: " + drummer.instrument)

输出结果为:

1
2
年龄:  27
喜欢的乐器: Drums

多继承实例

1
2
3
4
5
6
7
8
9
10
11
12
interface IParent1 {
v1: number
}

interface IParent2 {
v2: number
}

interface Child extends IParent1, IParent2 { }

var Iobj: Child = { v1: 12, v2: 23 }
console.log("value 1: " + Iobj.v1 + " value 2: " + Iobj.v2)

输出结果为:

1
value 1: 12 value 2: 23

类实现接口

类可以使用 implements 关键字实现接口,确保类符合接口定义的契约。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义接口
interface ILoan {
interest: number; // 利率
}

// 类实现接口
class AgriLoan implements ILoan {
interest: number;
rebate: number; // 回扣

constructor(interest: number, rebate: number) {
this.interest = interest;
this.rebate = rebate;
}
}

var loan = new AgriLoan(10, 1);
console.log("利率: " + loan.interest + "%,回扣: " + loan.rebate);

输出结果为:

1
利率: 10%,回扣: 1

一个类也可以同时实现多个接口,接口之间用逗号分隔。

鸭子类型(Duck Typing)

TypeScript 的类型兼容性基于"结构子类型"(Structural Subtyping),也就是常说的"鸭子类型":如果一个对象具有接口所要求的全部属性,并且类型兼容,那么它就被认为实现了该接口,无论它是否显式声明。换句话说,“如果它走起来像鸭子、叫起来像鸭子,那么它就是鸭子”。

1
2
3
4
5
6
7
8
9
10
11
interface Point {
x: number;
y: number;
}

// 没有显式声明实现 Point,但结构兼容
var p = { x: 10, y: 20, z: 30 };

// p 比 Point 多了 z 属性,但仍然可以赋值给 Point 类型的变量
var point: Point = p;
console.log(point.x + ", " + point.y);

鸭子类型让 TypeScript 在保持类型安全的同时具备良好的灵活性:只要结构匹配就允许赋值,而不强制要求显式的继承或实现关系。

类是面向对象编程(OOP)的核心概念,它是一种模板或蓝图,用于创建具有相同属性和方法的对象。TypeScript 完全支持面向对象编程,提供了类、继承、访问修饰符等特性。类封装了数据(属性)和行为(方法),使得代码更加模块化、可复用和易维护。通过类,我们可以创建多个具有相同结构的对象,这些对象称为类的实例。

TypeScript 使用 class 关键字定义类。一个类可以包含以下成员:

  • 字段(Field):类中声明的变量,表示对象的属性
  • 构造函数(Constructor):类实例化时调用的特殊方法,用于初始化对象
  • 方法(Method):类中定义的函数,表示对象的行为

语法格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class class_name {
// 字段声明
field1: type;
field2: type;

// 构造函数
constructor(parameters) {
// 初始化代码
}

// 方法
methodName(): return_type {
// 方法实现
}
}

字段与构造函数

类的字段是存储对象数据的地方,构造函数在对象创建时自动调用,用于初始化字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 定义 Car 类
class Car {
// 字段:描述汽车的属性
engine: string;

// 构造函数:在创建对象时初始化 engine
constructor(engine: string) {
this.engine = engine;
}

// 方法:显示发动机信息
disp(): void {
console.log("发动机型号: " + this.engine);
}
}

// 创建类的实例
var car = new Car("V8 发动机");

// 访问字段
console.log("读取发动机: " + car.engine);

// 调用方法
car.disp();

运行结果:

1
2
读取发动机: V8 发动机
发动机型号: V8 发动机

说明:

  • this 关键字指向当前类的实例
  • 构造函数的参数名可以与字段名相同,通过 this.field 区分
  • 使用 new 关键字创建类的实例

方法

方法是类中定义的函数,描述对象的行为。方法可以访问类的字段,也可以调用其它方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Calculator {
value: number;

constructor(initial: number) {
this.value = initial;
}

// 普通方法
add(n: number): void {
this.value += n;
}

// 返回值方法
current(): number {
return this.value;
}
}

var calc = new Calculator(10);
calc.add(5);
console.log("当前值: " + calc.current());

运行结果:

1
当前值: 15

存取器(Getter / Setter)

TypeScript 支持通过 getset 关键字定义存取器,把对属性的访问包装成方法调用,从而在读写时加入校验逻辑。使用存取器时,通常把真正的存储字段设为私有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Employee {
private _fullName: string = "";

// getter:读取时执行
get fullName(): string {
return this._fullName;
}

// setter:写入时执行校验
set fullName(newName: string) {
if (newName && newName.length > 0) {
this._fullName = newName;
} else {
console.log("名字不能为空");
}
}
}

var emp = new Employee();
emp.fullName = "Alice"; // 调用 setter
console.log(emp.fullName); // 调用 getter

emp.fullName = ""; // 触发校验,名字不会改变
console.log(emp.fullName);

运行结果:

1
2
3
Alice
名字不能为空
Alice

只带有 get 不带有 set 的存取器会被自动推断为 readonly

静态成员

使用 static 关键字定义的成员属于类本身,而不是类的实例。可以直接通过类名访问,不需要创建实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
class StaticMem {
// 静态属性
static num: number;

// 静态方法
static disp(): void {
console.log("num 值为 " + StaticMem.num);
}
}

// 直接通过类名访问静态成员
StaticMem.num = 12;
StaticMem.disp();

运行结果:

1
num 值为 12

静态成员常用于定义类的常量、工具方法或单例模式。在静态方法内部只能访问静态成员,不能直接访问实例成员,也不能使用 this 指向实例。

instanceof 运算符

instanceof 用于判断对象是否是某个类的实例。

1
2
3
4
5
6
7
class Person {
}

var obj = new Person();
var isPerson = obj instanceof Person;

console.log("obj 是 Person 类的实例吗? " + isPerson);

运行结果:

1
obj 是 Person 类的实例吗? true

访问修饰符

访问修饰符是 TypeScript 面向对象编程的核心特性之一,用于控制类成员(属性、方法、构造函数)的可见性。通过访问修饰符,可以实现封装,保护类的内部实现细节。

面向对象编程的三大原则之一是封装。封装意味着将数据和操作数据的方法隐藏起来,对外只暴露必要的接口。访问修饰符就是实现封装的手段,它控制类成员的可见范围。TypeScript 提供三种访问修饰符:publicprotectedprivate,以及一个相关修饰符 readonly

修饰符作用域

修饰符类内部子类类外部
public可以访问可以访问可以访问
protected可以访问可以访问不能访问
private可以访问不能访问不能访问

使用场景:

  • public — 公开方法/属性,如用户姓名、登录方法
  • protected — 子类可见方法,如计算公式、模板方法
  • private — 内部实现细节,如数据库连接、内部计算

public 修饰符

public 是默认的访问修饰符,表示成员可以在任何地方被访问。无论是类内部、子类还是类的外部,都可以访问 public 成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 定义动物类
class Animal {
// 使用 public 修饰 name 属性(可以省略,默认就是 public)
public name: string;

// 构造函数
public constructor(name: string) {
this.name = name;
}

// 公开的说话方法
public speak(): void {
console.log(this.name + " 发出声音");
}
}

// 创建实例
var animal = new Animal("动物");

// 在类外部访问 public 属性
console.log(animal.name);

// 在类外部调用 public 方法
animal.speak();

运行结果:

1
2
动物
动物 发出声音

如果不写访问修饰符,TypeScript 会默认使用 public,因此 public 可以省略,但为了代码清晰,建议明确声明。

private 修饰符

private 表示私有成员,只能在定义它的类内部访问。子类和类的外部都不能访问 private 成员。这常用于隐藏类的内部实现细节,保护数据不被意外修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 定义银行账户类
class BankAccount {
// 使用 private 修饰余额,只能在类内部访问
private balance: number;

// 构造函数
constructor(initialBalance: number) {
this.balance = initialBalance;
}

// 存款方法
public deposit(amount: number): void {
if (amount > 0) {
this.balance += amount; // 类内部可以访问 private 属性
console.log("存款成功,当前余额: " + this.balance);
}
}

// 获取余额
public getBalance(): number {
return this.balance; // 类内部可以访问 private 属性
}
}

// 创建账户实例
var account = new BankAccount(1000);

// 存款
account.deposit(500);

// 通过公共方法获取余额
console.log("余额: " + account.getBalance());

// 错误:在类外部不能直接访问 private 属性
// console.log(account.balance); // 编译错误!

运行结果:

1
2
存款成功,当前余额: 1500
余额: 1500

将类的内部状态设为 private,通过 public 方法提供受控的访问途径,这是实现封装的标准做法。

protected 修饰符

protected 表示受保护成员,可以在类内部和子类中访问。类的外部不能直接访问 protected 成员。这在需要让子类继承父类的某些功能,同时隐藏实现细节时非常有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 定义人员基类
class Person {
// 使用 protected 修饰 name,子类可以访问
protected name: string;

constructor(name: string) {
this.name = name;
}
}

// 定义员工类,继承 Person
class Employee extends Person {
// 部门是私有的
private department: string;

constructor(name: string, department: string) {
super(name); // 调用父类构造函数
this.department = department;
}

// 自我介绍方法
public introduce(): string {
// 子类可以访问 protected 成员 name
return "我是 " + this.name + ",在 " + this.department + " 工作";
}
}

// 创建员工实例
var emp = new Employee("Alice", "技术部");

console.log(emp.introduce());

// 错误:在类外部不能访问 protected 成员
// console.log(emp.name); // 编译错误!

运行结果:

1
我是 Alice,在 技术部 工作

protected 常用于定义子类需要使用的属性,但不应该暴露给外部的值。

readonly 修饰符

readonly 用于将属性设置为只读。只能在声明时或构造函数中赋值,之后不能修改。这在定义常量或标识符时非常有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 定义用户类
class User {
// 使用 readonly 修饰的属性只能在初始化时赋值
// 用户 ID
readonly id: number;
// 用户名
readonly name: string;

constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}

// 创建用户实例
var user = new User(1, "Alice");

console.log("用户: " + user.id + ", " + user.name);

// 错误:不能修改 readonly 属性
// user.id = 2; // 编译错误!
// user.name = "Bob"; // 编译错误!

运行结果:

1
用户: 1, Alice

readonlyprivate 不冲突,可以同时使用 readonlyprivate,既不能修改也不能从外部访问。readonly 也可以与 publicprotected 组合使用。

参数属性

TypeScript 提供了参数属性(Parameter Properties)的简写语法。可以在构造函数的参数上直接使用访问修饰符,自动创建并初始化属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 定义点类
class Point {
// 在构造函数参数上直接使用修饰符
// 相当于同时声明属性并赋值
constructor(
// public 修饰:创建公开属性 x
public x: number,
// public 修饰:创建公开属性 y
public y: number,
// private 修饰:创建私有属性 z
private z: number
) {
// 构造函数体可以为空,属性已自动创建
}

// 计算三维度总和
public sum(): number {
// 可以在类内部访问所有属性
return this.x + this.y + this.z;
}
}

// 创建点实例
var point = new Point(1, 2, 3);

// 公开属性可以从外部访问
console.log("x: " + point.x);
console.log("y: " + point.y);

// 调用方法
console.log("总和: " + point.sum());

// 错误:私有属性不能从外部访问
// console.log(point.z); // 编译错误!

运行结果:

1
2
3
x: 1
y: 2
总和: 6

参数属性可以大幅简化类的定义,避免重复的声明和赋值代码。

访问修饰符使用建议

  • 默认修饰符:不写修饰符时默认为 public
  • 构造函数:构造函数也可以使用访问修饰符,控制实例化权限
  • readonly 组合readonly 可以与 publicprotectedprivate 组合使用
  • 编译影响:访问修饰符仅在编译时检查,运行时无效

最佳实践是尽可能使用最严格的访问修饰符,只暴露必要的公开接口,将内部实现设为 privateprotected。默认使用 private,需要子类访问时使用 protected,需要完全公开时使用 public

抽象类

抽象类是 TypeScript 面向对象编程中的重要概念。它是一种不能被直接实例化的类,只能作为基类供其他类继承。抽象类主要用于定义子类的公共属性和方法,为子类提供一个统一的结构和行为模板。

抽象类是一种介于普通类和接口之间的类型。它既有接口的特征(定义方法签名),又有类的特征(可以有具体的方法实现和构造函数)。使用 abstract 关键字声明抽象类和抽象方法。

抽象类的特性:

  • abstract 方法:子类必须实现
  • 具象方法:子类可以直接使用
  • 可作为类型:接收子类实例

抽象类基础

抽象类可以包含抽象方法和具象方法(已有实现的方法)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 使用 abstract 关键字声明抽象类
// 抽象类不能直接实例化,只能作为基类
abstract class Animal {
// 动物的名字属性
name: string;

// 构造函数
constructor(name: string) {
this.name = name;
}

// 抽象方法:使用 abstract 修饰,没有方法体
// 子类必须实现这个方法
abstract speak(): void;

// 具象方法:有具体实现的方法
// 子类可以直接继承使用,不需要重写
move(): void {
console.log(this.name + " 在移动");
}
}

// 尝试实例化抽象类会报错
// var animal = new Animal("动物"); // 错误:不能实例化抽象类

// 定义 Dog 类继承 Animal
class Dog extends Animal {
// 子类必须实现父类的抽象方法 speak()
speak(): void {
console.log(this.name + " 汪汪汪!");
}
}

// 创建 Dog 实例
var dog = new Dog("旺财");
dog.speak(); // 调用子类实现的方法
dog.move(); // 继承父类的具象方法

运行结果:

1
2
旺财 汪汪汪!
旺财 在移动

抽象方法没有方法体(只有方法签名),子类必须实现该方法;具象方法有具体实现,子类可以直接继承使用。

抽象方法

抽象方法是抽象类中只声明但不实现的方法。子类继承抽象类后,必须实现所有抽象方法,否则会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 定义抽象类 Shape(图形)
abstract class Shape {
// 抽象方法:只有声明,没有实现
// 子类必须实现这两个方法
abstract area(): number;
abstract perimeter(): number;

// 具象方法:可以使用抽象方法
// 这个方法调用了抽象方法,子类继承后可以正常使用
describe(): void {
console.log("面积: " + this.area().toFixed(2));
}
}

// 定义矩形类继承 Shape
class Rectangle extends Shape {
// 矩形的宽度
width: number;
// 矩形的高度
height: number;

// 构造函数
constructor(width: number, height: number) {
super(); // 调用父类构造函数
this.width = width;
this.height = height;
}

// 实现抽象方法:计算面积
area(): number {
return this.width * this.height;
}

// 实现抽象方法:计算周长
perimeter(): number {
return 2 * (this.width + this.height);
}
}

// 创建矩形实例
var rect = new Rectangle(4, 5);

// 调用继承的 describe 方法,内部会调用子类的 area 方法
rect.describe();
console.log("周长: " + rect.perimeter());

运行结果:

1
2
面积: 20.00
周长: 18

具象方法可以调用抽象方法。这是因为当具象方法被调用时,子类已经实现了抽象方法,所以可以正常执行。这正是"模板方法模式"的体现:抽象类定义骨架,子类提供具体实现。

抽象类作为类型

抽象类可以作为参数类型使用。这意味着函数可以接受任何抽象类的子类实例,实现了多态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 定义抽象类 Animal
abstract class Animal {
// 抽象方法:所有动物都会发出声音
abstract speak(): void;
}

// 定义 Cat 类继承 Animal
class Cat extends Animal {
// 实现抽象方法
speak(): void {
console.log("喵喵喵!");
}
}

// 定义 Dog 类继承 Animal
class Dog extends Animal {
// 实现抽象方法
speak(): void {
console.log("汪汪汪!");
}
}

// 定义函数参数类型为抽象类 Animal
// 这个函数可以接受任何 Animal 的子类实例
function makeSpeak(animal: Animal): void {
animal.speak();
}

// 传入不同的子类实例,实现不同的行为(多态)
makeSpeak(new Cat());
makeSpeak(new Dog());

// 抽象类类型数组:可以存储不同子类实例
var animals: Animal[] = [new Cat(), new Dog()];

运行结果:

1
2
喵喵喵!
汪汪汪!

抽象类作为类型时,实际执行的是子类的方法实现,这就是面向对象的多态特性。

抽象类与接口的区别

抽象类和接口都用于定义类型规范,但有一些关键区别。理解它们的差异有助于在实际开发中做出正确的选择。

特性抽象类接口
实例化不能直接实例化不能直接实例化
方法实现可以有具体实现只能有声明(TypeScript 3.6+ 可有默认实现)
成员修饰符可以添加 public、protected、private只能有 readonly
继承单继承(只能 extends 一个类)多实现(可以 implements 多个接口)
构造函数可以有构造函数不能有构造函数

选择建议:需要共享代码使用时用抽象类;需要定义规范/契约时用接口;需要多继承时用接口。当多个类需要共享代码和逻辑时,使用抽象类;当只需要定义规范和契约时,使用接口。

完整示例:支付系统

下面是一个使用抽象类实现的支付系统示例,展示了抽象类在实际项目中的应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 定义抽象支付类
abstract class Payment {
// 抽象方法:处理支付,子类必须实现
abstract process(amount: number): boolean;

// 具象方法:验证支付信息
// 所有子类都可以使用这个方法
validate(): void {
console.log("验证支付信息");
}
}

// 信用卡支付类
class CreditCardPayment extends Payment {
// 信用卡号码
cardNumber: string;

// 构造函数
constructor(cardNumber: string) {
super(); // 调用父类构造函数
this.cardNumber = cardNumber;
}

// 实现抽象方法:处理信用卡支付
process(amount: number): boolean {
console.log("处理信用卡支付: " + amount);
return true;
}
}

// PayPal 支付类
class PayPalPayment extends Payment {
// PayPal 邮箱
email: string;

// 构造函数
constructor(email: string) {
super();
this.email = email;
}

// 实现抽象方法:处理 PayPal 支付
process(amount: number): boolean {
console.log("处理 PayPal 支付: " + amount);
return true;
}
}

// 使用多态处理不同的支付方式
var payments: Payment[] = [
new CreditCardPayment("1234"),
new PayPalPayment("test@example.com")
];

// 遍历处理每种支付方式
for (var _i = 0, payments_1 = payments; _i < payments_1.length; _i++) {
var payment = payments_1[_i];
// 调用继承的验证方法
payment.validate();
// 调用子类的处理方法
payment.process(100);
}

运行结果:

1
2
3
4
验证支付信息
处理信用卡支付: 100
验证支付信息
处理 PayPal 支付: 100

抽象类常用于框架设计和业务逻辑分层,例如支付处理、用户认证、数据持久化等场景。

抽象类注意事项

  • 不能实例化:抽象类不能直接使用 new 创建实例,必须通过子类继承
  • 必须实现抽象方法:子类如果不实现所有抽象方法,会报错
  • 可以有构造函数:抽象类可以定义构造函数,子类需要调用 super()
  • 单继承:一个类只能继承一个抽象类(单继承)
  • 访问修饰符:抽象方法可以使用 publicprotected 修饰符

在设计类层次结构时,优先考虑使用抽象类来共享代码和定义规范。

类继承与多态

继承允许创建一个类(子类)从另一个类(父类)获取属性和方法。子类可以复用父类的代码,还可以扩展或重写父类的行为。多态则让同一接口在不同子类上表现出不同的行为。

类的继承

使用 extends 关键字实现继承。子类会获得父类所有 publicprotected 成员。

基本语法:

1
2
3
class child_class extends parent_class {
// 子类新增的属性和方法
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Animal {
name: string;

constructor(name: string) {
this.name = name;
}

speak(): void {
console.log(this.name + " 发出声音");
}
}

class Dog extends Animal {
breed: string;

constructor(name: string, breed: string) {
super(name); // 调用父类构造函数
this.breed = breed;
}

speak(): void {
console.log(this.name + " 汪汪汪!");
}
}

var dog = new Dog("旺财", "金毛");
dog.speak();

运行结果:

1
旺财 汪汪汪!

super 关键字

super 用于调用父类的方法和构造函数。当子类定义了自己的构造函数时,必须在使用 this 之前调用 super(),以完成父类的初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Shape {
color: string;

constructor(color: string) {
this.color = color;
}

describe(): string {
return "这是一个 " + this.color + " 的图形";
}
}

class Circle extends Shape {
radius: number;

constructor(color: string, radius: number) {
super(color);
this.radius = radius;
}

// 重写父类方法
describe(): string {
// 调用父类方法并扩展
return super.describe() + ",半径是 " + this.radius;
}

area(): number {
return Math.PI * this.radius * this.radius;
}
}

var circle = new Circle("红色", 5);
console.log(circle.describe());
console.log("面积: " + circle.area().toFixed(2));

运行结果:

1
2
这是一个 红色 的图形,半径是 5
面积: 78.54

方法重写(Override)

子类可以重写(Override)父类的方法,即在子类中定义与父类同名的方法,实现自己的行为。使用 super 关键字可以在重写的方法中调用父类的同名方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 父类
class PrinterClass {
doPrint(): void {
console.log("父类的 doPrint() 方法");
}
}

// 子类:重写父类方法
class StringPrinter extends PrinterClass {
doPrint(): void {
// 调用父类的方法
super.doPrint();

// 子类自己的逻辑
console.log("子类的 doPrint() 方法");
}
}

var obj = new StringPrinter();
obj.doPrint();

运行结果:

1
2
父类的 doPrint() 方法
子类的 doPrint() 方法

多态

多态是指子类的实例可以赋值给父类类型,调用同一方法时由实际对象的类型决定执行哪个实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Animal {
name: string;
constructor(name: string) { this.name = name; }
speak(): void {
console.log(this.name + " 发出声音");
}
}

class Cat extends Animal {
speak(): void {
console.log(this.name + " 喵喵喵!");
}
}

class Dog extends Animal {
speak(): void {
console.log(this.name + " 汪汪汪!");
}
}

// 多态:数组中存储不同子类的实例
var animals: Animal[] = [
new Cat("小白"),
new Dog("旺财"),
new Animal("动物")
];

// 调用同一方法,不同子类有不同实现
for (var _i = 0, animals_1 = animals; _i < animals_1.length; _i++) {
var animal = animals_1[_i];
animal.speak();
}

运行结果:

1
2
3
小白 喵喵喵!
旺财 汪汪汪!
动物 发出声音

instanceof 检查

使用 instanceof 可以在运行时检查实例的类型,从而在多态场景下做差异化处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Rectangle {
width: number;
height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
area(): number {
return this.width * this.height;
}
}

class Circle {
radius: number;
constructor(radius: number) {
this.radius = radius;
}
area(): number {
return Math.PI * this.radius ** 2;
}
}

var shapes = [new Rectangle(4, 5), new Circle(3)];

for (var _i = 0, shapes_1 = shapes; _i < shapes_1.length; _i++) {
var shape = shapes_1[_i];
if (shape instanceof Rectangle) {
console.log("矩形面积: " + shape.area());
} else if (shape instanceof Circle) {
console.log("圆形面积: " + shape.area().toFixed(2));
}
}

运行结果:

1
2
矩形面积: 20
圆形面积: 28.27

多重继承与多层继承

TypeScript 不支持多继承(一个类继承多个类),但支持多层继承(A 继承 B,B 继承 C)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 根类
class Root {
str: string;
}

// 子类:继承 Root
class Child extends Root {
}

// 叶子类:继承 Child(多层继承)
class Leaf extends Child {
}

var leaf = new Leaf();
leaf.str = "hello";
console.log("str 值: " + leaf.str);

运行结果:

1
str 值: hello

protected 成员在继承中的可见性

protected 成员在子类中可见,但类外部不可见,这让它成为继承场景下共享内部状态的常用选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Person {
protected name: string;

constructor(name: string) {
this.name = name;
}
}

class Employee extends Person {
private department: string;

constructor(name: string, department: string) {
super(name);
this.department = department;
}

public introduce(): string {
// 可以访问 protected 成员
return "我是 " + this.name + ",在 " + this.department + " 工作";
}
}

var emp = new Employee("Alice", "技术部");
console.log(emp.introduce());

// console.log(emp.name); // 错误:protected 外部不可访问

运行结果:

1
我是 Alice,在 技术部 工作

继承与多态要点

  • 继承extends 关键字实现
  • super:用于调用父类构造函数与方法
  • 多态:同一接口在不同子类上呈现不同实现
  • protected:子类可见,外部不可见

混入(Mixin)

混入(Mixin)是一种代码复用模式,用于将多个独立的功能模块混入到一个类中。TypeScript 通过泛型函数返回扩展类的方式实现 Mixin,弥补了单继承无法复用多个来源行为的不足。

基本概念

Mixin 的核心是一个接收基类、返回扩展类的泛型函数。其中 Constructor 类型约束用于描述"任意可被继承的类"。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Constructor 类型:描述任意可实例化的类
type Constructor<T = {}> = new (...args: any[]) => T;

// Mixin 函数:接收基类,返回扩展后的新类
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date();
};
}

// 基类
class User {
constructor(public name: string) {}
}

// 混入后得到新类
const TimestampedUser = Timestamped(User);
const user = new TimestampedUser("Alice");

console.log(user.name); // Alice
console.log(user.createdAt instanceof Date); // true

运行结果:

1
2
Alice
true

注意:Mixin 不修改原始类,而是返回一个全新的扩展类,原始的 User 类不受影响。

组合多个 Mixin

将多个 Mixin 函数嵌套调用,即可将多份能力依次叠加到同一个类上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
type Constructor<T = {}> = new (...args: any[]) => T;

// Mixin 1:添加时间戳
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date();
};
}

// Mixin 2:添加序列化
function Serializable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
serialize(): string {
return JSON.stringify(this);
}
};
}

// Mixin 3:添加日志(依赖 serialize 方法)
function Loggable<TBase extends Constructor<{ serialize(): string }>>(Base: TBase) {
return class extends Base {
log(): void {
console.log("[LOG]", this.serialize());
}
};
}

class Product {
constructor(public name: string, public price: number) {}
}

// 依次叠加三个 Mixin
const AdvancedProduct = Loggable(Serializable(Timestamped(Product)));

const p = new AdvancedProduct("Phone", 999);
p.log();
console.log(p.createdAt instanceof Date); // true

运行结果:

1
2
[LOG] {"name":"Phone","price":999,"createdAt":"2026-..."}
true

Mixin 与接口结合

Mixin 返回的类可以声明实现某个接口,使消费方通过接口类型操作混入后的对象,而不依赖具体实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
type Constructor<T = {}> = new (...args: any[]) => T;

// 定义能力接口
interface ISerializable {
serialize(): string;
}

interface ICloneable<T> {
clone(): T;
}

// Mixin 实现接口
function Serializable<TBase extends Constructor>(Base: TBase) {
return class extends Base implements ISerializable {
serialize(): string {
return JSON.stringify(this);
}
};
}

function Cloneable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
clone() {
return Object.assign(
Object.create(Object.getPrototypeOf(this)),
this
);
}
};
}

class Article {
constructor(public title: string, public content: string) {}
}

const RichArticle = Cloneable(Serializable(Article));

const a1 = new RichArticle("TypeScript 入门", "正文内容...");
const a2 = a1.clone();
a2.title = "TypeScript 进阶";

console.log(a1.serialize());
// {"title":"TypeScript 入门","content":"正文内容..."}

console.log(a2.serialize());
// {"title":"TypeScript 进阶","content":"正文内容..."}

console.log(a1.title === a2.title); // false(克隆后独立修改)

运行结果:

1
2
3
{"title":"TypeScript 入门","content":"正文内容..."}
{"title":"TypeScript 进阶","content":"正文内容..."}
false

带约束的 Mixin

通过泛型约束,可以限定 Mixin 只能混入满足特定条件的类,避免运行时缺少必要属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Constructor<T = {}> = new (...args: any[]) => T;

// 约束:基类必须有 id 和 name 属性
type WithIdAndName = Constructor<{ id: number; name: string }>;

function Printable<TBase extends WithIdAndName>(Base: TBase) {
return class extends Base {
print(): void {
console.log(`[${this.id}] ${this.name}`);
}
};
}

class Item {
constructor(public id: number, public name: string) {}
}

// 正确:Item 满足约束
const PrintableItem = Printable(Item);
const item = new PrintableItem(42, "Keyboard");
item.print(); // [42] Keyboard

// 错误示例(编译器会阻止):
// class NoId { constructor(public name: string) {} }
// const Bad = Printable(NoId); // 错误:NoId 缺少 id 属性

运行结果:

1
[42] Keyboard

Mixin 与继承的对比

维度继承(extends)Mixin
来源数量只能继承一个父类可叠加任意数量
耦合程度子类与父类强耦合每个 Mixin 独立,低耦合
复用粒度复用整个类的能力按需复用单一功能
类型安全原生支持需借助泛型约束保证
适用场景强 “is-a” 关系横切关注点(日志、序列化、缓存等)

混入要点

  • 核心模式:Mixin 是接收基类、返回扩展类的泛型函数,Constructor<T> 是标准约束类型
  • 能力叠加:嵌套调用多个 Mixin 函数即可将多份能力组合到同一个类
  • 接口结合:Mixin 返回的类可以实现接口,消费方只需依赖接口而非具体类
  • 泛型约束:通过约束 TBase 可以限定 Mixin 的适用范围,在编译期阻止错误使用
  • 适用场景:日志、序列化、克隆、时间戳等横切关注点,优于多层继承

小结

接口与类共同构成了 TypeScript 面向对象编程的两条主线:接口负责声明形状与契约,强调"结构兼容"的鸭子类型;类负责封装数据与行为,并通过访问修饰符、抽象类、继承与多态提供从内部封装到外部复用的完整能力。当单继承无法覆盖日志、序列化等横切关注点时,混入(Mixin)以泛型函数的形式叠加独立能力,是继承之外更灵活的复用手段。合理地组合这些特性,可以让代码既保持类型安全,又具备良好的可扩展性与可维护性。