Skip to content

Error when overriding arrow function in superclass with regular method in subclass to correctly bind this to superΒ #61882

Open
@Zacqary

Description

@Zacqary

πŸ”Ž 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 getEntry as a regular method instead of an arrow function
  • Changing the subclasses to define getEntry as an arrow function instead of a regular method
  • Changing the name of the superclass's arrow function to something like _getEntry and calling super._getEntry in the subclass's getEntry

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 #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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions