TypeScript中建模类型层级结构的最实用方案
TypeScript中抽象基类与多层子类型多态模型的最佳实现
问题背景
我需要构建一套包含抽象基类型、多层嵌套子类型的结构——部分子类型本身也是抽象类型,同时要定义多态函数:其中一部分是必须由子类实现的抽象函数,另一部分带有默认实现且允许子类重载。
在JavaScript中我用Class实现了一个示例(仅关注结构,无需在意语义):
class Polygon { constructor( name ) { this.name = name; } // 抽象方法(运行时断言) area() { assert.fail(`${this.constructor.name} must implement area()`); } // 带默认实现的方法 toString() { const props = Object.entries(this) .filter( ([k, v])=>k !== 'name' ) .map( ([k, v])=>`${k}:${v}` ) .join(", "); return `${this.name}{${props}}`; } } class Triangle extends Polygon { constructor( base, height ) { super('TRIANGLE'); this.base = base; this.height = height; } area() { return this.base*this.height/2; } } class Quadrilateral extends Polygon { constructor( name, base, height ) { super(name); this.base = base; this.height = height; } area() { return this.base*this.height; } } class Square extends Quadrilateral { constructor( side ) { super( 'SQUARE', side, side ); } area() { return this.base*this.height; } toString() { return `${this.name}{side:${this.base}}`; } }
但直接转成TypeScript会遇到两个核心问题:
name字段无法作为Polygon的合法类型判别式,必须定义type PolygonType = Triangle | Square这类联合类型来替代基类做未知子类型的类型约束;- Class的表达能力受限,无法利用TypeScript类型层面的元编程来定义更精确的类型结构。
如果改用TypeScript的type建模,又要放弃原型继承的多态性和Class的其他优势,因此想了解更合适的解决方案及社区推崇的实践。
可行解决方案及分析
方案1:TypeScript原生抽象类(改进版)
TypeScript原生支持abstract关键字,可以直接定义抽象基类、抽象方法,既保留Class的继承优势,又解决了JavaScript版本中抽象方法只能靠运行时断言的问题。
代码示例
abstract class Polygon { readonly name: string; // 添加字面量类型的判别式,解决类型守卫问题 readonly type: 'triangle' | 'quadrilateral' | 'square'; constructor(type: typeof Polygon.prototype.type, name: string) { this.type = type; this.name = name; } // 抽象方法:强制子类实现,编译时即可检测 abstract area(): number; // 带默认实现的方法,子类可重载 toString(): string { const props = Object.entries(this) .filter(([k]) => k !== 'name' && k !== 'type') .map(([k, v]) => `${k}:${v}`) .join(', '); return `${this.name}{${props}}`; } } class Triangle extends Polygon { base: number; height: number; constructor(base: number, height: number) { super('triangle', 'TRIANGLE'); this.base = base; this.height = height; } area(): number { return this.base * this.height / 2; } } // 中间层抽象子类型 abstract class Quadrilateral extends Polygon { base: number; height: number; constructor(type: 'quadrilateral' | 'square', name: string, base: number, height: number) { super(type, name); this.base = base; this.height = height; } } class Square extends Quadrilateral { constructor(side: number) { super('square', 'SQUARE', side, side); } area(): number { return this.base * this.height; } // 重载默认方法 toString(): string { return `${this.name}{side:${this.base}}`; } }
核心优势
- 编译时强制子类实现抽象方法,避免运行时错误;
- 保留原型继承的多态性,方法仅在原型上定义一次,内存效率高;
- 通过添加字面量类型的
type字段,可直接作为合法的类型判别式,支持类型守卫和联合类型操作; - 支持多层抽象子类型的层级结构。
方案2:类型别名+函数多态(纯类型层面建模)
如果需要更强的类型元编程能力(比如复杂的类型变换、组合),可以用TypeScript的类型别名、接口结合函数式编程来模拟多态,同时用函数实现默认逻辑。
代码示例
// 基础结构约束 type PolygonBase = { name: string; type: 'triangle' | 'square' | 'quadrilateral'; }; // 抽象方法的类型约束 type HasArea = { area: () => number; }; // 默认toString函数实现 function defaultToString<T extends PolygonBase>(obj: T): string { const props = Object.entries(obj) .filter(([k]) => k !== 'name' && k !== 'type') .map(([k, v]) => `${k}:${v}`) .join(', '); return `${obj.name}{${props}}`; } // 子类型定义 type Triangle = PolygonBase & HasArea & { base: number; height: number; type: 'triangle'; }; type Square = PolygonBase & HasArea & { side: number; type: 'square'; }; // 构造函数:封装实例创建与方法绑定 function createTriangle(base: number, height: number): Triangle & { toString: () => string } { const triangle: Triangle = { name: 'TRIANGLE', type: 'triangle', base, height, area: () => base * height / 2, }; return { ...triangle, toString: () => defaultToString(triangle), }; } function createSquare(side: number): Square & { toString: () => string } { const square: Square = { name: 'SQUARE', type: 'square', side, area: () => side * side, }; return { ...square, toString: () => `${square.name}{side:${square.side}}`, }; }
核心优势
- 完全利用TypeScript的类型系统,支持复杂的类型变换(如
Omit、Pick、泛型组合等); - 判别式字段
type天然合法,直接支持联合类型的类型守卫; - 实例结构完全由类型定义约束,灵活性更高。
劣势
- 放弃了原型继承,每个实例的方法都是独立绑定的,内存开销更大;
- 无法使用
instanceof做类型判断,只能依赖判别式或类型守卫。
方案3:混合式方案(Class+类型别名/接口)
结合前两种方案的优势,用Class实现运行时的继承和多态,同时用类型别名或接口增强类型层面的表达能力,兼顾运行时效率和类型灵活性。
代码示例
// 接口:用于类型层面的约束与元编程 interface PolygonInterface { name: string; type: string; area(): number; toString(): string; } // 抽象基类:实现运行时逻辑 abstract class Polygon implements PolygonInterface { readonly name: string; readonly type: 'triangle' | 'quadrilateral' | 'square'; constructor(type: typeof Polygon.prototype.type, name: string) { this.type = type; this.name = name; } abstract area(): number; toString(): string { // 默认实现 const props = Object.entries(this) .filter(([k]) => k !== 'name' && k !== 'type') .map(([k, v]) => `${k}:${v}`) .join(', '); return `${this.name}{${props}}`; } } // 联合类型:用于约束未知子类型 type PolygonType = Triangle | Square; class Triangle extends Polygon { base: number; height: number; constructor(base: number, height: number) { super('triangle', 'TRIANGLE'); this.base = base; this.height = height; } area(): number { return this.base * this.height / 2; } } // 类型元编程示例:提取子类型的非公共属性 type PolygonProps<T extends PolygonType> = Omit<T, keyof PolygonInterface>; type TriangleProps = PolygonProps<Triangle>; // { base: number; height: number }
核心优势
- 保留Class的原型继承和运行时多态性,内存效率高;
- 用接口/类型别名实现复杂的类型操作,满足元编程需求;
- 可通过联合类型
PolygonType实现未知子类型的类型约束,同时利用type字段做类型守卫。
方案推荐
- 优先选择改进版抽象类方案:如果你的核心需求是运行时多态性、代码复用,且类型元编程需求不复杂,这是TypeScript社区最常用、最符合面向对象范式的方案;
- 选择纯类型层面方案:如果核心需求是复杂的类型变换、元编程,且可以接受放弃原型继承的一些优势;
- 选择混合式方案:如果需要同时兼顾运行时效率和类型灵活性。
内容的提问来源于stack exchange,提问作者Blue Nebula
相关产品推荐
相关产品推荐

