在抽象类中需要两种方法之一,或同时需要这两种方法

Require either one of two methods, or both in abstract class

提问人:Kenya-West 提问时间:1/13/2023 最后编辑:T.J. CrowderKenya-West 更新时间:1/13/2023 访问量:80

问:

例如,我有这样的代码:

export abstract class AbstractButton {
    // Always have to provide this
    abstract someRequiredMethod(): void;

    // One need to provide one of these (or both)
    abstract setInnerText?(): void;
    abstract setInnerHTML?(): void;
}

我需要一个继任者来实现 or ,或两者兼而有之。setInnerText()setInnerHTML()

如何使用人类构建的最强大的类型系统来实现这一点?

TypeScript OOP 继承 方法 abstract-class

评论

1赞 katniss 1/13/2023
不可能 AFAIK。
0赞 T.J. Crowder 1/13/2023
类要求子类实现所有方法,或者它们本身是 .这包括您在上面标记为可选的那些:typescriptlang.org/play?#code/...abstractabstractabstract

答:

0赞 T.J. Crowder 1/13/2023 #1

我找到了一种获得编译时错误的方法,但它已经够丑陋了,您可能只想要求 mixin 或类似的东西,或者只是只进行运行时测试(从构造函数抛出),这毕竟会在子类开发的早期出现。(我在下面展示了一个运行时检查的示例,因为即使出于下面所述的原因,即使使用编译时检查,您也无论如何都需要它。AbstractButton

但我会详细说明我是如何设法让它给我一个编译时错误。

首先,你不能以这种方式定义那些可选的抽象方法,即使你有它们,它们也是必需的(操场示例)。因此,我们需要定义没有它们的基类,然后找到一种方法来确保子类至少有一个基类。所以现在我们的基类只是:?

abstract class AbstractButton {
    // Always have to provide this
    abstract someRequiredMethod(): void;
}

我不认为你可以纯粹做你想做的事情,但是你可以用一个 do-nothing 函数确保编译时错误,如果子类没有实现至少一个这些方法,这将导致类型错误。class ___ extends AbstractButton

首先,我们需要一个帮助程序映射类型来检查构造函数的原型是否具有以下任一方法:

type CheckRequired<Ctor> =
    [Ctor] extends [ { prototype: { setInnerText: () => void; } } ]
    ? Ctor
    : [Ctor] extends [ { prototype: { setInnerHTML: () => void; } } ]
    ? Ctor
    : {__error__: "AbtractButton subclasses must implement `setInnerText` and/or `setInnerHTML`"}};

我们可以使这些检查更加严格(例如,防止一个类实现一个有效但无效的 [invalid 因为它采用的参数与你想要的参数不同),但这是它的一般形状。setInnerTextsetInnerHTML

我们正在测试元组 (, not ) 这一事实很重要,稍后会详细介绍。[Ctor] extends ___Ctor extends ___

好的,现在我们需要一个函数,它什么都不做,但对它得到的东西进行类型检查:

function checkAbstractButtonSubclass<T>(cls: CheckRequired<T>) {}

现在,不幸的是,我们必须要求人们编写一个调用代码才能得到类型错误:

checkAbstractButtonSubclass(SomeButtonSubclass);

下面是一系列带有调用的类 — 请注意,调用会导致类型错误,因为没有实现 or :NeitherNeithersetInnerTextsetInnerHTML

class Both extends AbstractButton {
    someRequiredMethod() { }
    setInnerText() { }
    setInnerHTML() { }
}
checkAbstractButtonSubclass(Both); // <== No error, it provides both

class JustText extends AbstractButton {
    someRequiredMethod() { }
    setInnerText() { }
}
checkAbstractButtonSubclass(JustText); // <== No error, it provides one of them

class JustHTML extends AbstractButton {
    someRequiredMethod() { }
    setInnerHTML() { }
}
checkAbstractButtonSubclass(JustHTML); // <== No error, it provides one of them

class Neither extends AbstractButton {
    someRequiredMethod() { }
}
checkAbstractButtonSubclass(Neither); // <== Error, it doesn't provide either:
// Argument of type 'typeof Neither' is not assignable to parameter of type '{ __error__: "AbtractButton subclasses must implement `setInnerText` and/or `setInnerHTML`"; }'

必须编写一个毫无意义的函数调用来获得编译时类型错误显然不太理想,但我认为你不能没有它。我确实试图想出一种方法来使函数调用以某种方式有用,但除非您需要子类的注册步骤,否则我还没有想出一种方法。也许比我聪明的人会。:-)

由于人们会忘记编写该调用的代码,因此您需要构造函数执行运行时检查作为编译时检查的后盾(因此,如果编码人员不将调用写入),则在开发早期会失败,例如:AbstractButtoncheckAbstractButtonSubclass

constructor() {
    if (
        (!("setInnerText" in this) || typeof this.setInnerText !== "function") &&
        (!("setInnerHTML" in this) || typeof this.setInnerHTML !== "function")
    ) {
        throw new Error(
            "AbstractButton subclasses must implement either setInnerText or setInnerHTML. " +
            "You can use checkAbstractButtonSubclass(YourClass) to get a proactive compile-time " +
            "error for this instead of this runtime error."
        );
    }
}

游乐场链接

最后:那么,为什么实用程序类型要检查而不是仅仅测试?因为我们需要防止泛型类型参数被分发,因为它会破坏测试。通过将其包装在元组中,我们禁用了分发。[Ctor][{ prototype: { ___ } }]Ctor{ prototype: ____ }

但同样,这真的很笨拙和复杂,并且在构造函数中没有所有额外内容的简单运行时检查将在子类开发的早期出现,所以我想我可能会对此感到满意。AbstractButton

1赞 jsejcksn 1/13/2023 #2

您可以使用类型别名,该别名使用泛型类型参数来区分应需要哪种实现:

TS游乐场

type AbstractButton<T extends 'both' |  'html' | 'text' = 'both'> =
  & { someRequiredMethod(): void }
  & (
    T extends 'html' ? { setInnerHTML(): void }
    : T extends 'text' ? { setInnerText(): void }
    :  {
      setInnerText(): void;
      setInnerHTML(): void;
    }
  );

然后,您可以使用 implements 子句来确保一致性。以下是上面 TypeScript playground 链接中的一些演示性示例 — 查看更详尽的集合:

class B5 implements AbstractButton<'html'> { /* Error
      ~~
Class 'B5' incorrectly implements interface 'AbstractButton<"html">'.
  Type 'B5' is not assignable to type '{ setInnerHTML(): void; }'.
    Property 'setInnerHTML' is missing in type 'B5' but required in type '{ setInnerHTML(): void; }'.(2420) */
  someRequiredMethod(): void {}
}

class B6 implements AbstractButton<'html'> { // ok
  someRequiredMethod(): void {}
  setInnerHTML(): void {}
}

class B7 implements AbstractButton<'text'> { /* Error
      ~~
Class 'B7' incorrectly implements interface 'AbstractButton<"text">'.
  Type 'B7' is not assignable to type '{ setInnerText(): void; }'.
    Property 'setInnerText' is missing in type 'B7' but required in type '{ setInnerText(): void; }'.(2420) */
  someRequiredMethod(): void {}
}

class B10 implements AbstractButton { /* Error
      ~~
Class 'B10' incorrectly implements interface 'AbstractButton<"both">'.
  Type 'B10' is not assignable to type '{ setInnerText(): void; setInnerHTML(): void; }'.
    Property 'setInnerText' is missing in type 'B10' but required in type '{ setInnerText(): void; setInnerHTML(): void; }'.(2420)
input.tsx(7, 7): 'setInnerText' is declared here. */
  someRequiredMethod(): void {}
  setInnerHTML(): void {}
}

class B12 implements AbstractButton { // ok
  someRequiredMethod(): void {}
  setInnerHTML(): void {}
  setInnerText(): void {}
}

评论

0赞 T.J. Crowder 1/13/2023
如果他们不需要成为 .我想他们这样做是出于某种原因,但希望不是。AbstractButtonclass
0赞 jsejcksn 1/13/2023
^@T.J.Crowder:是的,抽象类的真正价值仍然让我难以理解。我了解名义价值(例如支票)......你知道其他人吗?instanceof
0赞 T.J. Crowder 1/13/2023
我认为是部分实施。问题中的类是完全抽象的(但可能只是精简了),但一般来说,我唯一会使用一个类的情况是,如果我有具体的方法来实现,并且只有几个特定于子类的方法。话虽如此,我不记得我上一次写是什么时候了......