Error when overriding arrow function in superclass with regular method in subclass to correctly bind `this` to super
🔎 Search Terms
is:issue "defines it as instance member function"
🕗 Version & Regression Information
5.1.6
⏯ Playground Link
https://www.typescriptlang.org/play/?#code/FAFwngDgpgBAogOxAJzDAvDAhgsBuUSWAWSzACMpEU1NrUYAfGAVwQBMoAzASwSnYFw0GACUoAZxYAbEAGUwCAMYB5ZAEEJipQB4AkhM3aANGMkyQAPgwwDR5TCgAPEFA4SYKFrAD8MAArIAPYAtjwSUDriUrLWAFxmMSBCRDAAItx8PCA8QQgKymr2unZaytaY0RYFqhplusAwtob1xo3wSKjAlikipBRQGbwI2bn52kX1+i3aFYnVE3XaOu2lJu39lPRg3QTAStJYEh7iAObhNNPFji5u7B5esMxcWNIRNi9vUNYA3u0QyB4ADcsK4YMgoOcJDQEqQIDpoYCEKdTAAKACUGGsQyyOTyNUmyzW5Tm-AA7jA4Ri9k0ICxyNIeEpwZCLlBkNsbKj2k0eOwEoi+CieTBTlAQDiRniEAkMVj0pkpWMCUtlFd6pZ2pj0L8RSAABbhAB0EKhNCNERAqL5pjFEsVozy6IITQAvjSYHSGUzReLtrK+QKUELtdZNoMHdKVcV1bMuSK+Vr5X8mk0lHlob77cNHQgbAbjaaLqgjXbrexnQmuDBUQBCO2S3OYlOppoQkAsZB5ticYYCbAeYnM5yudyeZDeGB+QKhcKRHuKgTxVgcReCEWukXtzt5huRsbU9ru4Cb4DOCBBZAgGAHI4nVmIsD+emM4e3MdnYtgHSPXW0l8+kc2hZts3KtoGMCCsiWoJDOYQRDo4bbH+qbbl22Bklg2SQSw0DIKWfqdGA5aVm6J7AGeTgXleN6HMcZhmqgcjskC7I3KO9wMV+OifBEKFeq+IFEeWQZIqc6KwmQWxETALZtuKO44XhBEgKBfKkTAm6bkAA
💻 Code
Given this example of trying to create a superclass that allows you to register functions, and subclasses that define whether these functions must be async or sync:
type Entry = () => any;
type MaybeEntry = Entry | undefined;
type EntrySyncOrAsync<IsAsync> = IsAsync extends true ? Promise<Entry> : Entry;
type MaybeEntrySyncOrAsync<IsAsync> = IsAsync extends true ? Promise<MaybeEntry> : MaybeEntry;
class Registry<IsAsync extends true | false = false> {
private registry: Map<string, () => EntrySyncOrAsync<IsAsync>> = new Map();
public registerEntry = (
id: string,
getter: () => EntrySyncOrAsync<IsAsync>
) => {
this.registry.set(id, getter);
};
// Define getEntry as an arrow function. If we don't use an arrow function, calls to
// super.getEntry from a subclass will bind `this` to the subclass. We want to keep it
// bound to the superclass so we can access the private `registry`
public getEntry: (id: string) => MaybeEntrySyncOrAsync<IsAsync> = (
id
) => {
const getDefinition = this.registry.get(id); // Without using arrow function, this errors because `this.registry` is undefined
if (!getDefinition) {
return undefined as IsAsync extends true ? Promise<undefined> : undefined;
}
return getDefinition();
};
}
export class RegistryPublic extends Registry<true> {
// Define getEntry as a regular class method. If we use an arrow function to please the Typescript compiler,
// then `this` will get rebound to the subclass instead of the superclass
public async getEntry(
id: string
): Promise<MaybeEntry> {
return await super.getEntry(id);
}
}
export class RegistryServer extends Registry<false> {
public getEntry(id: string): MaybeEntry {
return super.getEntry(id);
}
}
🙁 Actual behavior
RegistryPublic and RegistryServer error with Class 'Registry<type entry>' defines instance member property 'getEntry', but extended class 'RegistryPublic' defines it as instance member function.(2425)
🙂 Expected behavior
No error. This code executes perfectly fine at runtime.
Ways to alter this code to get Typescript to accept it are:
- Changing the superclass to define
getEntryas a regular method instead of an arrow function - Changing the subclasses to define
getEntryas an arrow function instead of a regular method - Changing the name of the superclass's arrow function to something like
_getEntryand callingsuper._getEntryin the subclass'sgetEntry
But any of these approaches cause this to end up rebound to the subclass, losing access to this.registry and causing the code to crash at runtime.
Additional information about the issue
This is similar to https://github.com/Microsoft/TypeScript/issues/27965, but I don't think it's a duplicate specifically because of this behavior around this binding. There doesn't seem to be another way to properly bind this to a superclass than this arrow function overriding pattern.
Note: I'm not a TS team member
This is quite strange code and I don't see direct evidence of a TS bug. Can you demonstrate the runtime crashes you say occur if you alter the code in the ways you mention? I don't see any code in your example that would have such an effect, and I played with the code in the playground using instances of RegistryPublic where the superclass has a method instead of an arrow function and and can't get anything like that to happen.
The arrow function doesn't affect whether this is bound to the subclass or superclass; it only affects whether it's bound to the instance on which you call getEntry() or whether it's bound to the instance on which it was originally created. But even that won't matter if you always call registry.getEntry() in the usual way. Where's the bug?
This error intentional to prevent a common JS class footgun:
class Base {
prop = function() { console.log("base"); }
}
class Derived extends Base {
prop() { console.log("derived"); }
}
const d = new Derived();
// prints "base", not "derived"
d.prop();
This issue has been marked as "Working as Intended" and has seen no recent activity. It has been automatically closed for house-keeping purposes.