I have this declared class in some third party library that I can't modify:
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;
}[];
}
And I want to customize it in this way with new class extending from that declaration:
export class CustomKeyboardSensor extends KeyboardSensor {
public static activators = [
{
eventName: "onKeyDown" as const,
moreCode: "...",
},
{
eventName: "onKeyUp" as const,
moreCode: "...",
},
];
}
I am obviously getting an error that the type property is incompatible:
Types of property 'eventName' are incompatible.
How can I define new interface/type for the declare class where I can modify the eventName
type?
CodePudding user response:
It isn't happy because here you've said that it can only be onKeyDown
:
eventName: "onKeyDown";
It should probably be just string
:
eventName: string;
And if you want it to always start with on
, you can even do that, too:
eventName: `on${string}`;
CodePudding user response:
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 ofFB
(and, consequently, the base classE
) is present except for the memberb
which we will be redefining later.new (...args:ConstructorParameters<typeof FB>): InstanceType<typeof FB>
reminds the compiler thatF
is a constructor, whereasargs:ConstructorParameters
andInstanceType
utilities free us to change the base class without the need to update the derived constructor type.b: ...
readds the omittedb
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).