如何用不兼容的类型覆盖基 class 的 属性?

How to override a property of a base class with an incompatible type?

我在一些我无法修改的第三方库中声明了 class:

export declare class KeyboardSensor implements SensorInstance {
  private props;
  constructor(props: KeyboardSensorProps);
  private attach;
  private handleStart;
  private detach;
  static activators: {
      eventName: "onKeyDown";
      handler: (event: React.KeyboardEvent, { keyboardCodes, onActivation, }: KeyboardSensorOptions) => boolean;
  }[];
}

我想用从该声明扩展的新 class 以这种方式自定义它:

export class CustomKeyboardSensor extends KeyboardSensor {
  public static activators = [
    {
      eventName: "onKeyDown" as const,
      moreCode: "...",
    },
    {
      eventName: "onKeyUp" as const,
      moreCode: "...",
    },
  ];
}

我显然收到类型 属性 不兼容的错误:

Types of property 'eventName' are incompatible.

如何为 declare class 定义新的 interface/type 并修改 eventName 类型?

不开心因为这里你说只能onKeyDown:

      eventName: "onKeyDown";

应该是 string:

      eventName: string;

如果您希望它始终以 on 开头,您甚至可以这样做:

      eventName: `on${string}`;
TypeScript 中的

Class inheritance 不允许派生 classes 比基础 classes 更广泛。根据手册:

TypeScript enforces that a derived class is always a subtype of its base class.

这意味着 class 的成员 extends 您覆盖的基础 class 是 covariant(因为派生 class 始终是 subclass 或简单地说,更具体)。考虑以下 - 覆盖有效,因为 "A" 是更广泛的联合 "A" | "B":

的子类型
class A {
    static b : Array<{ c: "A" | "B" }> = []
}

class B extends A {
    static b : Array<{ c: "A" }> = [] // OK
}

然而,相反的结果会导致可分配性错误,因为被覆盖的成员不是逆变的:

class C {
    static b : Array<{ c: "C" }> = []
}

class D extends C {
    static b : Array<{ c: "C" | "D" }> = [] // Type '"D"' is not assignable to type '"C"'
}

后一个示例在语义上等同于您的情况:eventName 被声明为 string literal type onKeyDown,这意味着不允许任何和所有扩展 classes扩大它,因此错误。


您的选择是有限的,但是,有一个 hacky 方法可以解决这个问题。假设你有以下基数 class E:

class E {
    constructor(public e : string) {}
    static b : Array<{ c: "E" }> = []
    static s : number = 42;
}

首先,让我们声明派生的 class 并以某种方式命名它,让它成为 FB:

class FB extends E {
    constructor(public f: number) {
        super(f.toString());
    }
}

到目前为止很简单,对吧?精彩部分来了:

const F: Omit<typeof FB,"b"> & { 
    new (...args:ConstructorParameters<typeof FB>): InstanceType<typeof FB> 
    b: Array<{ c: "E" | "D" }>
} = FB;

有很多东西要打开。通过将声明的派生 class 分配给一个变量,我们创建了一个 class expression const F = FB;,它使 class 的 static 部分能够通过显式输入 F 变量来输入。现在对于类型本身0:

  • Omit<typeof FB, "b"> 确保编译器知道 FB 的静态部分(因此,基础 class E)存在,除了成员 b 我们稍后将重新定义它。
  • new (...args:ConstructorParameters<typeof FB>): InstanceType<typeof FB> 提醒编译器 F 是构造函数,而 args:ConstructorParametersInstanceType 实用程序使我们无需更改基础 class更新派生的构造函数类型。
  • b: ... 将省略的 b 成员重新添加到派生 class 的静态端,同时扩展它(请注意,由于不涉及 class 继承,因此有没有错误)。

以上所有内容都在 compile-time 期间修复了 b 成员,但我们仍然需要确保静态成员在运行时可用,类似这样(请参阅 getOwnPropertyDescriptor / defineProperty 上的 MDN详情):

const descr = Object.getOwnPropertyDescriptor(E, "b")!;
Object.defineProperty(F, "b", { 
    ...descr, 
    value: [...descr.value, { c: "D" }] 
});

最后,让我们检查一下是否一切正常:

console.log(
    new F(42), // FB
    new F(42).e, // string
    F.b, // { c: "E" | "D"; }[]
    F.s // number
);

// at runtime:
// FB: {
//   "e": "42",
//   "f": 42
// },
// "42",  
// [{ "c": "D" }],  
// 42 

游乐场with examples above | applied to your case


0 请注意,我们经常不得不使用 typeof FB 类型的查询——如果我们之前没有声明 class 并选择快捷方式const F: { ... } = class ...,我们将无法在显式键入变量时引用 class 本身(如果我们尝试,编译器会抱怨循环引用)。