TypeScript如何实现仅支持同类型操作的Operable接口?
我写了一段变量统计代码,核心逻辑重复多次,打算抽象封装。统计基于不可变值对象(当前是Money和Time),需要定义Operable接口实现抽象,目标是让这些值对象能接入以下Average泛型类:
class Average<T extends Operable> { private value: T; private count: number; public constructor(init: T) { this.value = init; this.count = 0; } public add(value: T): void { this.value = this.value.add(value); this.count++; } public getAverage(): T { return this.value.divide(Math.max(1, this.count)); } }
最初定义的Operable接口如下:
interface Operable { add(value: Operable): Operable; divide(value: number): Operable; }
但这个接口存在两个问题:
- 允许跨类型操作,比如
const m = new Money(); const t = new Time(); m.add(t);这种非法操作无法被TypeScript拦截 - 在
Average.add方法中报错:
Type 'Operable' is not assignable to type 'T'.
'Operable' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Operable'.
后来尝试用this类型优化接口:
interface Operable { add(value: this): this; divide(value: number): this; }
这解决了Average类的报错,但值对象(比如Money)的实现又出现类型不兼容错误:
Property 'add' in type 'Money' is not assignable to the same property in base type 'Operable'.
Type '(money: Money) => Money' is not assignable to type '(value: this) => this'.
Type 'Money' is not assignable to type 'this'.
'Money' is assignable to the constraint of type 'this', but 'this' could be instantiated with a different subtype of constraint 'Money'.
Money和Time的具体定义:
class Money { private readonly amount: number; private readonly currency: string; public constructor(amount: number, currency: string) { this.amount = amount; this.currency = currency; } public add(value: Money): Money { return new Money(this.amount + value.amount, this.currency); } public divide(value: number): Money { return new Money(this.amount / value, this.currency); } } class Time { private readonly duration: number; public constructor(duration: number) { this.duration = duration; } public add(value: Time): Time { return new Time(this.duration + value.duration); } public divide(value: number): Time { return new Time(this.duration / value); } }
请问在TypeScript中如何实现这种仅支持同类型操作的接口,适配不可变值对象场景?
可以通过泛型接口+递归约束来实现,确保add方法只能接收同类型实例,同时解决所有类型兼容问题。
1. 定义泛型Operable接口
将Operable改为泛型接口,让每个实现类指定自身类型作为泛型参数,明确操作的类型边界:
interface Operable<T> { add(value: T): T; divide(value: number): T; }
2. 修改值对象实现
让Money和Time实现对应泛型接口,将自身类型传入泛型参数:
class Money implements Operable<Money> { private readonly amount: number; private readonly currency: string; public constructor(amount: number, currency: string) { this.amount = amount; this.currency = currency; } public add(value: Money): Money { return new Money(this.amount + value.amount, this.currency); } public divide(value: number): Money { return new Money(this.amount / value, this.currency); } } class Time implements Operable<Time> { private readonly duration: number; public constructor(duration: number) { this.duration = duration; } public add(value: Time): Time { return new Time(this.duration + value.duration); } public divide(value: number): Time { return new Time(this.duration / value); } }
3. 调整Average泛型类约束
修改Average类的泛型约束为T extends Operable<T>,通过递归约束确保T的add方法返回和接收的都是自身类型:
class Average<T extends Operable<T>> { private value: T; private count: number; public constructor(init: T) { this.value = init; this.count = 0; } public add(value: T): void { this.value = this.value.add(value); this.count++; } public getAverage(): T { return this.value.divide(Math.max(1, this.count)); } }
验证效果
现在可以正常使用,同时TypeScript会自动拦截跨类型操作:
// 合法操作 const moneyAvg = new Average(new Money(0, "USD")); moneyAvg.add(new Money(10, "USD")); console.log(moneyAvg.getAverage()); // Money { amount: 10, currency: "USD" } const timeAvg = new Average(new Time(0)); timeAvg.add(new Time(60)); console.log(timeAvg.getAverage()); // Time { duration: 60 } // 非法操作,TypeScript直接报错:类型Time的参数不能赋值给类型Money的参数 // moneyAvg.add(new Time(60));
这种方案既保证了值对象的同类型操作约束,又让Average类能正确进行类型推断,完美适配不可变值对象的场景。
内容的提问来源于stack exchange,提问作者carlosV2

