我有一个代码,我正在对一些变量进行一些统计。它基本上是相同的代码,但一遍又一遍地重复,所以我决定提供一个很好的抽象。
问题是这些统计数据是在值对象上执行的,因此我需要为这些对象定义一个接口以允许抽象。
我的 2 个当前值对象是
Money
和 Time
,我希望能够将它们插入到如下所示的类中(抽象代码):
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
接口,Money
和 Time
需要从中扩展,所以我继续尝试创建它:
interface Operable {
add(value: Operable): Operable;
divide(value: number): Operable;
}
然而,这并不是我想要的,因为它允许
Money
和 Time
交叉操作(例如 const m = new Money(); const t = new Time(); m.add(t);
)。除此之外,我的 IDE 在 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'.
后来通过 Google 搜索,我了解到可以在界面中使用
this
来引用实例类型,因此我将界面更改如下:
interface Operable {
add(value: this): this;
divide(value: number): this;
}
这会清除
Average.add
中的错误,但将其推送到值对象的方法(这是 Money.add
的示例):
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'.
有没有办法让这种构造在 Typescript 中工作?非常感谢!
编辑:请注意,我的值对象是不可变的,因此将
Average.add
方法更改为 this.value.add(value);
不是一个选项。
EDit2:以下是
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);
}
}
将您的可操作类型更改为:
type Operable = {
add<T extends Operable>(value: T): T;
divide<T extends Operable>(value: number): T;
}
Operable
实现仅处理“自身”。
您发现了多态
this
类型,这是一种递归约束形式,并且非常接近您想要的;调用者确实会受到限制,因此 money.add(time)
和 time.add(money)
会失败(除非 time
和 money
恰好在结构上兼容,但它们并不兼容)。但是 implementations 也受到类似的限制,并且 this
并不意味着“声明此方法的类”,它意味着“当前 this
上下文的类型,包括可能的子类”。所以失败的原因是:
class BadTime {
constructor(private readonly duration: number) { }
add(value: this): this {
return new BadTime(this.duration + value.duration); // error
}
}
是因为编译器无法阻止你这样做:
class ReallyBadTime extends BadTime {
hello() { console.log("hi") };
}
const rbt = new ReallyBadTime(10).add(new ReallyBadTime(20));
// ^? const rbt: ReallyBadTime
rbt.hello(); // RUNTIME ERROR
在
ReallyBadTime
内部,this
需要有 hello()
方法,因此 add()
方法必须返回具有 hello()
方法的内容。但是从 add()
继承的 BadTime
的实现并没有这样做。 rbt.hello()
处的运行时错误是 add()
实现中的编译器错误向您发出的警告。
无论如何,如果您希望能够实现
add()
和 divide()
以便子类不会假装做它们不能做的事情,您需要将 this
更改为更通用的递归约束。这是一种方法:
interface Operable<T extends Operable<T>> {
add(value: T): T;
divide(value: number): T;
}
约束
T extends Operable<T>
表达了“本身”的限制。该部分本质上与多态 this
类型在幕后的工作方式相同。区别在于 this
的类型 argument自动缩小为调用对象类型(例如
ReallyBadTime
),而 Operable<Time>
始终将 Time
作为 T
,即使在子类中也是如此。
然后您只需将约束传播到
Average
:
class Average<T extends Operable<T>> {
// ✂ snip ⋯ snip ✂ //
}
你就差不多完成了。如果需要,您可以在 implements
和
Money
上使用 an
Time
子句以确保您已正确实现它:
class Money implements Operable<Money> {
// ✂ snip ⋯ snip ✂ //
}
class Time implements Operable<Time> {
// ✂ snip ⋯ snip ✂ //
}
但这完全是可选的。
implements
子句不会影响类型;它所做的只是在你犯了错误时在课堂上警告你。没有它你也可以过得很好(但是只有当你尝试使用一个不正确实现的Money
或Time
作为new Average()
的参数时,你所犯的任何错误才会被注意到):
class Money {
// ✂ snip ⋯ snip ✂ //
}
class Time {
// ✂ snip ⋯ snip ✂ //
}
好吧,让我们确保它有效:
const moneyAverage = new Average(new Money(10, "USD"));
moneyAverage.add(new Money(20, "GBP"));
moneyAverage.add(new Money(30, "EUR"));
const m = moneyAverage.getAverage();
console.log(m.toString()); // 30.00 USD // hmm, might not be right
const timeAverage = new Average(new Time(10));
timeAverage.add(new Time(20));
timeAverage.add(new Time(30));
const t = timeAverage.getAverage();
console.log(t.toString()) // 30 shakes of a lamb's tail // also not sure
timeAverage.add(new Money(40, "USD")); // error
// -----------> ~~~~~~~~~~~~~~~~~~~~
// Argument of type 'Money' is not assignable to parameter of type 'Time'.
const foo = { add() { return 1 }, divide() { return 2 } };
new Average(foo); // error!
// -------> ~~~
// Argument of type '{ add(): number; divide(): number; }' is not assignable to
// parameter of type 'Operable<{ add(): number; divide(): number; }>'.
看起来不错。除了货币和时间单位的怪异之外,应该编译的代码也会被编译器编译。当您犯错误时,例如混合使用
time
和 money
,或者当您对无效的 new Average()
调用 Operable
时,您会收到有关它们的信息错误。