如何在TypeScript中模拟密封抽象类实现穷尽性检查?
在TypeScript中模拟Kotlin密封类的穷尽性检查
Kotlin的密封类通过限制子类范围,让编译器能做穷尽性分支检查。在TypeScript中,我们可以通过几种方式实现类似效果,避免新增子类时遗漏分支处理。
问题背景
你的Expr类继承体系中,新增Neg子类后,TypeScript不会自动提示Add#rightAssoc中的instanceof检查不完整,可能导致未处理分支抛出运行时错误。以下是几种解决思路:
方案1:使用区分联合类型(推荐)
区分联合类型是TypeScript实现密封逻辑的原生方案,通过唯一的判别属性(如type字段)让编译器识别所有可能的类型分支,强制进行穷尽性检查。
// 定义Expr为所有表达式类型的联合 type Expr = NumExpr | AddExpr | NegExpr; // 每个表达式类型包含唯一的type判别符 interface NumExpr { type: "num"; value: number; } interface AddExpr { type: "add"; left: Expr; right: Expr; } interface NegExpr { type: "neg"; inner: Expr; } // 实现eval方法 function evalExpr(expr: Expr): number { switch (expr.type) { case "num": return expr.value; case "add": return evalExpr(expr.left) + evalExpr(expr.right); case "neg": return -evalExpr(expr.inner); // 新增类型时,这里会提示缺少case分支 default: // 用never类型确保所有分支被覆盖 const _exhaustiveCheck: never = expr; throw new Error(`Unexpected expression type: ${_exhaustiveCheck}`); } } // 实现rightAssoc方法 function rightAssoc(expr: Expr): Expr { switch (expr.type) { case "num": return expr; case "add": { const leftAssoc = rightAssoc(expr.left); // 递归处理左操作数的右结合 if (leftAssoc.type === "add") { return rightAssoc({ type: "add", left: leftAssoc.left, right: { type: "add", left: leftAssoc.right, right: expr.right } }); } return { type: "add", left: leftAssoc, right: rightAssoc(expr.right) }; } case "neg": return { type: "neg", inner: rightAssoc(expr.inner) }; default: const _exhaustiveCheck: never = expr; throw new Error(`Unexpected expression type: ${_exhaustiveCheck}`); } }
优势:
- 原生支持穷尽性检查,新增类型时编译器自动报错
- 代码更简洁,避免类继承的冗余
- 类型推导更精准
方案2:限制类继承范围 + 联合类型
如果需要保留类的面向对象结构,可以通过私有构造函数限制子类只能在同一文件中定义,同时将Expr类型声明为所有子类的联合,实现编译时检查。
// 将Expr定义为所有子类的联合类型 type Expr = Num | Add | Neg; // 抽象基类,私有构造函数限制外部继承 abstract class ExprBase { private constructor() {} abstract eval(): number; abstract rightAssoc(): Expr; // 允许子类调用构造函数 protected static create<T extends ExprBase>(this: new (...args: any[]) => T, ...args: any[]): T { return new this(...args); } } class Num extends ExprBase { constructor(public value: number) { super(); } override eval(): number { return this.value; } override rightAssoc(): Num { return new Num(this.value); } } class Add extends ExprBase { constructor(public left: Expr, public right: Expr) { super(); } override eval(): number { return this.left.eval() + this.right.eval(); } override rightAssoc(): Expr { const leftAssoc = this.left.rightAssoc(); if (leftAssoc instanceof Num) { return new Add(leftAssoc, this.right.rightAssoc()); } else if (leftAssoc instanceof Add) { return new Add(leftAssoc.left, new Add(leftAssoc.right, this.right).rightAssoc()); } else if (leftAssoc instanceof Neg) { // 新增Neg后必须处理,否则编译器报错 return new Add(leftAssoc, this.right.rightAssoc()); } else { // never类型确保穷尽性 const _exhaustiveCheck: never = leftAssoc; throw new Error(`Unexpected expression type: ${_exhaustiveCheck}`); } } } class Neg extends ExprBase { constructor(public inner: Expr) { super(); } override eval(): number { return -this.inner.eval(); } override rightAssoc(): Neg { return new Neg(this.inner.rightAssoc()); } }
优势:
- 保留类的封装和继承特性
- 私有构造函数防止外部新增子类
- 联合类型确保编译时穷尽性检查
方案3:穷尽性检查辅助函数
如果不想修改现有类结构,可以添加一个辅助函数,利用never类型在编译时检测未覆盖的分支。
// 辅助函数:接收never类型参数,确保所有分支被覆盖 function assertNever(value: never): never { throw new Error(`Unexpected value: ${value}`); } // 在Add的rightAssoc中使用 class Add extends Expr<number> { // ...其他代码... public override rightAssoc(): Expr<number> { const expr = this.left.rightAssoc(); if (expr instanceof Num) { return new Add(expr, this.right.rightAssoc()); } else if (expr instanceof Add) { return new Add(expr.left, new Add(expr.right, this.right).rightAssoc()); } else if (expr instanceof Neg) { // 新增Neg后必须添加此分支,否则assertNever报错 return new Add(expr, this.right.rightAssoc()); } // 如果有未处理的分支,expr不是never类型,这里编译报错 return assertNever(expr); } }
优势:
- 最小化修改现有代码
- 编译时检测遗漏分支
- 实现简单,无需重构
内容的提问来源于stack exchange,提问作者Aadit M Shah
相关产品推荐
相关产品推荐

