本节目标:掌握
implements让一个类同时满足多份接口契约的写法,理清接口继承接口、接口与抽象类、类型别名三者的边界;理解类名「实例类型 + 构造器类型」的双重身份,并据此写出类型安全的 mixin 函数,用函数组合绕开 JavaScript 的单继承限制。读完本节,你能在真实工程里把「契约」与「实现」干净地分开。
6.3 接口实现与 mixin
前两节我们把类当成「数据的容器」和「可继承的骨架」。但工程里还有另一类需求:一个对象要同时满足好几套契约——它既可以被序列化,又可以被比较,还要能打日志。JavaScript 只允许单继承,一条 extends 链装不下这些正交的能力。TypeScript 用两件工具解决它:implements 负责声明类满足哪些契约,mixin 负责注入多份实现。
6.3.1 implements:类对契约的承诺
interface 在 interface 定义对象结构
里已经讲过,它描述「一个对象长什么样」。类用 implements 声明自己符合某个接口:
interface Serializable {
serialize(): string;
}
interface Comparable<T> {
compareTo(other: T): number;
}
class Version implements Comparable<Version> {
constructor(public readonly major: number, public readonly minor: number) {}
compareTo(other: Version): number {
return this.major - other.major || this.minor - other.minor;
}
}
console.log(new Version(1, 2).compareTo(new Version(1, 10))); // -8
关键点:implements 是纯粹的编译期检查,编译后完全消失。它在运行时不留任何痕迹,也不影响原型链。它的价值是「早失败」:
class Bad implements Serializable {
// error TS2420: Class 'Bad' incorrectly implements interface 'Serializable'.
// Property 'serialize' is missing in type 'Bad' but required in type 'Serializable'.
}
注意一个容易混淆的地方:implements 只检查类的实例侧。静态成员不参与接口实现,构造函数也不参与。下面这段是合法的:
interface HasId { id: string; }
class Entity implements HasId {
static create() { return new Entity(); }
id = crypto.randomUUID();
}
HasId 里没有 create,但 Entity 多一个静态方法不影响它满足契约——接口描述的是实例形状。
另一个细节:接口里声明的方法,类可以用属性来实现,反之亦然(因为函数也是值):
interface Greeter { greet(): string; }
class A implements Greeter {
greet = () => "hi"; // 属性实现方法签名,合法
}
class B implements Greeter {
greet(): string { return "hi"; }
}
但注意 A 的写法会让每个实例各持有一个函数,而 B 的方法挂在原型上共享。性能敏感的场景(大量实例)要留意这个差别。
6.3.2 一个类实现多个接口
接口可以写成一串,用逗号分隔:
interface Serializable { serialize(): string; }
interface Loggable { log(): void; }
interface Disposable { dispose(): void; }
class Session implements Serializable, Loggable, Disposable {
private disposed = false;
constructor(public readonly token: string) {}
serialize(): string {
return JSON.stringify({ token: this.token });
}
log(): void {
console.log(`session=${this.token} disposed=${this.disposed}`);
}
dispose(): void {
this.disposed = true;
}
}
这是 TypeScript 相对单继承最重要的补偿:extends 只能写一个类,implements 可以写任意多个接口。接口只要求签名一致,不要求同源,所以「正交能力」可以自由组合。
常见坑是同名成员签名冲突:
interface A { run(x: number): void; }
interface B { run(x: string): void; }
class C implements A, B {
run(x: number): void {} // error TS2416: Property 'run' in type 'C' is not assignable to the same property in base type 'B'.
}
一个类里不可能有两个同名方法,所以当多份契约对同一成员给出不兼容签名时,编译器会拒绝。这类冲突必须在设计接口时就避免。
6.3.3 接口、抽象类、类型别名怎么选
这是初学者最常问的问题。三者的边界可以这样记:
| 维度 | interface | abstract class | type 别名 |
|---|---|---|---|
| 能否含实现 | 不能(只能签名) | 能 | 不能 |
能否被 extends | 能(多继承接口) | 能(单继承) | 不能作为父类 |
能否被 implements | 能 | 能 | 能(对象类型) |
| 能否实例化 | 不能 | 不能 | 不适用 |
| 声明合并 | 支持 | 不支持 | 不支持 |
| 适合表达 | 纯契约、可扩展 | 骨架 + 默认实现 | 联合、映射、工具类型 |
实用判据:
- 只要「形状」、不要实现 →
interface。 - 需要一份默认实现、或需要
protected成员 →abstract class。 - 要表达联合类型、条件类型、模板字面量 →
type(接口做不到)。 - 想让第三方库能通过声明合并给自己加字段 → 必须
interface。
一个典型搭配是「接口定契约、抽象类给默认实现」:
interface Shape {
area(): number;
}
abstract class BaseShape implements Shape {
abstract area(): number;
describe(): string {
return `面积约 ${this.area().toFixed(2)}`;
}
}
class Circle extends BaseShape {
constructor(private readonly r: number) { super(); }
area(): number { return Math.PI * this.r ** 2; }
}
console.log(new Circle(2).describe()); // 面积约 12.57
BaseShape implements Shape 保证抽象类本身也没跑偏,子类再 extends 拿到默认的 describe。这是「契约 + 骨架」的标准组合,类型层面的更多技巧可参考 TypeScript 高级类型
。
6.3.4 接口继承接口
接口之间也能 extends,而且可以一次继承多个:
interface Named { name: string; }
interface Aged { age: number; }
interface Person extends Named, Aged {
email?: string;
}
const p: Person = { name: "小明", age: 18 };
接口多继承是安全的,因为接口没有实现,不存在「菱形继承冲突」。当两个父接口有同名成员时,只有类型不兼容才会报错:
interface X { id: string; }
interface Y { id: number; }
interface Z extends X, Y {} // error TS2320: Interface 'Z' cannot simultaneously extend types 'X' and 'Y'.
反过来,类也可以被接口 extends,此时接口继承的是类的实例形状(含 private / protected 成员):
class Point { constructor(public x: number, public y: number) {} }
interface Point3D extends Point {
z: number;
}
const q: Point3D = { x: 1, y: 2, z: 3 };
这种写法较少用,因为接口继承类会把类的私有成员也变成契约的一部分,反而限制实现自由度。多数情况下用 implements 或直接定义接口更清晰。
6.3.5 类名的双重身份:实例类型与构造器类型
要理解 mixin,必须先把这个机制搞清楚:类名在类型位置表示「实例类型」,而构造器本身的类型要写 typeof 类名。
class User {
constructor(public name: string) {}
}
type UserInstance = User; // 实例类型:{ name: string }
type UserCtor = typeof User; // 构造器类型:new (name: string) => User
const c: UserCtor = User; // 合法
const u: UserInstance = new c("小明");
更常用的是把它写成构造器签名,用来表示「任意可以 new 出 T 的东西」:
type Ctor<T> = new (...args: any[]) => T;
function create<T>(C: Ctor<T>): T {
return new C();
}
new (...args: any[]) => T 这个类型是后面所有 mixin 的类型基石。注意 any[] 在这里是必要的(虽然我们一直在讲少用 any):因为基类的构造参数各不相同,用 any[] 才能让任意构造器都满足约束。更严格的替代是用 unknown[],但那会让传入的构造器类型不兼容,实践中 any[] 是通行做法。关于 any 的取舍,见 any·unknown·never·void 与类型断言
。
6.3.6 mixin:用函数混合行为
现在进入本节的重头戏。JavaScript 只允许单继承,但我们可以写一个接收基类、返回扩展后新类的函数——这就是 mixin:
type Ctor<T = {}> = new (...args: any[]) => T;
// 可序列化能力
function Serializable<TBase extends Ctor>(Base: TBase) {
return class extends Base {
toJSON(): object {
return { ...this };
}
};
}
// 可打时间戳能力
function Timestamped<TBase extends Ctor>(Base: TBase) {
return class extends Base {
createdAt = new Date();
ageInSeconds(): number {
return (Date.now() - this.createdAt.getTime()) / 1000;
}
};
}
用法就是层层包裹:
class Model {
constructor(public id: string) {}
}
const EnhancedModel = Timestamped(Serializable(Model));
const m = new EnhancedModel("m-1");
console.log(JSON.stringify(m)); // {"id":"m-1","createdAt":"2026-09-06T02:00:00.000Z"}
console.log(typeof m.id); // string
console.log(m.ageInSeconds()); // 0.001...
这里 toJSON(): object 返回一个全新的普通对象,所以 JSON.stringify(m) 会调用它并把结果序列化——这是 toJSON 的标准用法。千万不要写成 toJSON() { return JSON.stringify(this); }:JSON.stringify 会再次调用 this.toJSON(),直接陷入无限递归并抛出 RangeError: Maximum call stack size exceeded。
三个机制在同时工作:
- 匿名类表达式
class extends Base动态继承传入的基类,从而把能力「叠加」上去。 - 泛型约束
<TBase extends Ctor>保证Base一定可以被new,且extends Base合法。泛型约束的完整规则见 泛型约束(extends) 。 - 返回类型由 TS 推断:
EnhancedModel的实例类型是三层叠加后的结果,m.id、m.toJSON、m.ageInSeconds全都有类型,不需要手写接口。
对比单继承的写法——class EnhancedModel extends Model 只能继承一条链,想再加一个 Comparable 就得插到链中间,顺序还被锁死。mixin 则可以任意组合、任意顺序(顺序只影响同名方法的覆盖关系)。
6.3.7 带约束的 mixin:需要基类提供前置能力
如果某个能力依赖基类已有的成员,就要在约束里声明出来:
interface HasId { id: string; }
function Loggable<TBase extends Ctor<HasId>>(Base: TBase) {
return class extends Base {
log(): void {
console.log(`[entity] id=${this.id}`); // 依赖 HasId.id
}
};
}
class Entity { constructor(public id: string) {} }
const L = Loggable(Entity);
new L("e-1").log(); // [entity] id=e-1
// 传一个没有 id 的类会报错:
// error TS2345: Argument of type 'typeof Plain' is not assignable to parameter of type 'Ctor<HasId>'.
Ctor<HasId> 把「基类必须有 id: string」写进了类型约束,调用方一旦传错基类,编译期就报错。这是 mixin 相比「运行期猴子补丁」最大的优势。
几个必须知道的坑:
- 不能直接给 mixin 结果加
private成员:因为返回的是匿名类,私有成员无法在外部声明中引用。 protected成员跨 mixin 访问会受限:两个 mixin 想互相访问对方的protected成员时,由于它们不是同一声明,TypeScript 会报错。需要共享时改用public或引入一个接口。- 构造函数参数要一路透传:
class extends Base不写构造函数时自动转发,一旦写了就要super(...args),否则参数丢失。 - 不要嵌套过深:三层以上 mixin 的类型推导会变慢,报错信息也会变得难读。
6.3.8 装饰器也能做 mixin
TS 5.0 起,标准装饰器(--experimentalDecorators 之外的 stage 3 版本)可以在类上返回一个替换类,效果与 mixin 类似:
function addToString<T extends Ctor>(value: T, _ctx: ClassDecoratorContext) {
return class extends value {
toString(): string {
return `[instance ${this.constructor.name}]`;
}
};
}
@addToString
class Widget {}
但装饰器涉及元数据、执行时机与 emitDecoratorMetadata 等一整套机制,属于进阶内容,本节只需知道「装饰器可以返回新类」这条线索。更系统的用法请见 TypeScript 装饰器与元编程
。
在选型上,mixin 与装饰器、与传统的继承层次各有取舍,架构层面的讨论可参考 架构模式与设计模式的区别 ;如果你对面向对象的一般原理还想补课,面向对象基础 是合适的入口。
6.3.9 小结与下一节预告
本节讲了两条主线:implements 让类声明自己满足哪些契约(可多份、编译期检查、编译后消失),mixin 让类获得多份实现(函数包裹、构造器泛型、类型自动叠加)。选型口诀:
- 只描述形状 →
interface - 要一份默认实现 →
abstract class - 要正交能力的自由组合 →
mixin - 要表达联合/条件类型 →
type
到这里,「类与面向对象」这一章就讲完了。下一章开始进入 TypeScript 类型系统最实用的部分:如何用联合类型与字面量类型把「取值只能是这几个之一」写进类型,让非法状态在编译期就无法表达。
小结
implements是编译期检查,运行时无痕迹;它只约束实例侧,静态成员与构造函数不参与。- 一个类可以
implements多个接口,这是对单继承最重要的补偿;同名成员签名冲突会报 TS2416。 - 接口之间可以多继承(TS2320 只在类型不兼容时触发);接口也能
extends类,但会把私有成员也带进契约,少用。 - 接口 / 抽象类 / 类型别名的分工:纯形状用
interface,骨架加默认实现用abstract class,联合与条件类型用type。 - 类名有两重身份:类型位置是实例类型,
typeof 类名才是构造器类型;new (...args: any[]) => T是 mixin 的类型基石。 - mixin 就是「接收基类、返回新类」的函数;用
TBase extends Ctor约束基类,用Ctor<HasId>表达前置能力依赖。 - mixin 的坑:
private成员无法跨 mixin 共享、protected跨声明访问受限、构造函数参数要手动透传、嵌套别超过三层。 - 下一节进入第 7 章,看联合类型与字面量类型如何把「非法状态」从类型里彻底排除。
阅读导航:上一节:6.2 继承、抽象类与多态 · 下一节:7.1 联合类型与字面量类型 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。