Typescript 接口仅在同一实现上运行

问题描述 投票:0回答:2

我有一个代码,我正在对一些变量进行一些统计。它基本上是相同的代码,但一遍又一遍地重复,所以我决定提供一个很好的抽象。

问题是这些统计数据是在值对象上执行的,因此我需要为这些对象定义一个接口以允许抽象。

我的 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);
   }
}
typescript interface this implementation
2个回答
0
投票

将您的可操作类型更改为:

type Operable = {
   add<T extends Operable>(value: T): T;
   divide<T extends Operable>(value: number): T;
}

这是一个工作示例


0
投票

您正在寻找所谓的 F 有界量化,也称为 递归 约束。您希望

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
时,您会收到有关它们的信息错误。

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.