《TypeScript编程入门》6.3 接口实现与 mixin

本节讲解 TypeScript 中类与接口的两条连接方式:用 implements 让一个类同时承诺多份契约,用接口继承接口组合出更大的类型;并系统对比接口、抽象类与类型别名的适用边界。随后进入 mixin 模式,讲清如何用构造器泛型把多个行为混入同一个类,从而绕开单继承的限制,并给出类型安全写法的常见坑。读完本节,你能为复杂业务设计出契约清晰、复用灵活的类结构。

本节目标:掌握 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 接口、抽象类、类型别名怎么选

这是初学者最常问的问题。三者的边界可以这样记:

维度interfaceabstract classtype 别名
能否含实现不能(只能签名)能不能
能否被 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。

三个机制在同时工作:

  1. 匿名类表达式 class extends Base 动态继承传入的基类,从而把能力「叠加」上去。
  2. 泛型约束 <TBase extends Ctor> 保证 Base 一定可以被 new,且 extends Base 合法。泛型约束的完整规则见 泛型约束(extends) 。
  3. 返回类型由 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 联合类型与字面量类型 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「typescript」更多文章

  1. 《TypeScript高级编程》11.3 类型驱动架构与团队规范
  2. 《TypeScript高级编程》11.2 渐进式迁移与严格化路径
  3. 《TypeScript高级编程》11.1 TS 版本演进与 breaking changes