Class inheritance in TypeScript does not allow derived classes to be broader than base classes. As per the Handbook:
TypeScript enforces that a derived class is always a subtype of its base class.
This means that members of the class that extends
the base class that you override are covariant (as derived class is always a subclass of its base or, put simply, is more specific). Consider the following - the override works because "A"
is a subtype of a broader union "A" | "B"
:
class A {
static b : Array<{ c: "A" | "B" }> = []
}
class B extends A {
static b : Array<{ c: "A" }> = [] // OK
}
However, the opposite results in an assignability error because the overridden members are not contravariant:
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"'
}
The latter example is semantically equivalent to your case: eventName
is declared to be a string literal type onKeyDown
, meaning any and all extending classes are not allowed broaden it, hence the error.
Your options are limited, however, there is a hacky way to go around that. Suppose you have the following base class E
:
class E {
constructor(public e : string) {}
static b : Array<{ c: "E" }> = []
static s : number = 42;
}
First, let's declare the derived class and name it somehow, let it be FB
:
class FB extends E {
constructor(public f: number) {
super(f.toString());
}
}
Pretty simple so far, right? Here comes the juicy part:
const F: Omit<typeof FB,"b"> & {
new (...args:ConstructorParameters<typeof FB>): InstanceType<typeof FB>
b: Array<{ c: "E" | "D" }>
} = FB;
There is a lot to unpack. By assigning the declared derived class to a variable, we create a class expression const F = FB;
which enables the static part of the class to be typed via explicit typing of the F
variable. Now for the type itself0:
Omit<typeof FB, "b">
ensures the compiler knows the static side of FB
(and, consequently, the base class E
) is present except for the member b
which we will be redefining later.
new (...args:ConstructorParameters<typeof FB>): InstanceType<typeof FB>
reminds the compiler that F
is a constructor, whereas args:ConstructorParameters
and InstanceType
utilities free us to change the base class without the need to update the derived constructor type.
b: ...
readds the omitted b
member to the static side of the derived class while broadening it (note that as class inheritance is not involved, there is no error).
All the above fixes the b
member during compile-time, but we still need to make sure the static member is available at runtime with something like this (see MDN on getOwnPropertyDescriptor
/ defineProperty
for details):
const descr = Object.getOwnPropertyDescriptor(E, "b")!;
Object.defineProperty(F, "b", {
...descr,
value: [...descr.value, { c: "D" }]
});
Finally, let's check if everything works as expected:
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
Playground with examples above | applied to your case
0 Note that we often have to use the typeof FB
type query — if we haven't declared the class earlier and opted to shortcut to const F: { ... } = class ...
, we would not be able to refer to the class itself when explicitly typing the variable (if we tried, the compiler would complain of a circular reference).