Skip to content

Allow Generic Child Classes to Satisfy Only a Subset of Parent Constructor OverloadsΒ #52585

Open
@ITenthusiasm

Description

@ITenthusiasm

Suggestion

πŸ” Search Terms

  • constructor
  • Allow Generic Child Classes to Satisfy Only a Subset of Parent Constructor Overloads
  • overloads
  • extend constructor

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Currently, the following does not compile in TypeScript 4.9

interface ParentClassConstructor {
  new <T extends number>(arg: T): ParentClass;
  new <T extends string>(arg: T): ParentClass;
}

interface ParentClass {
  method(): void;
}

const ParentClass: ParentClassConstructor = class<T extends number | string> {
  constructor(arg: T) {
    // Do something with generic argument
  }

  method(): void {
    // Do anything
  }
};

class ChildClass<T extends number> extends ParentClass<T> {
  constructor(number: T) {
    // Do something with number argument
  }
}

Instead, I get the following error when trying to work with the ChildClass:

Type 'T' does not satisfy the constraint 'string'.
  Type 'number' is not assignable to type 'string'.

In fact, changing the T type parameter for ChildClass to T extends number | string also fails. It seems that if the generic type parameter of the child class does not satisfy each of the possible generic constructors in the parent constructor interface, then the compiler will fail.

My suggestion/feature request is that TypeScript would be updated to permit the use of child classes whose generic type parameters satisfy a subset (at least 1) of the generic overloads of the parent class constructor. This would allow the code above to successfully compile.

If it's worth noting, the following is already legal currently:

interface ParentClassConstructor {
  new (arg: number): ParentClass;
  new (arg: string): ParentClass;
}

interface ParentClass {
  method(): void;
}

const ParentClass: ParentClassConstructor = class {
  constructor(arg: number | string) {
    // Do something with argument
  }

  method(): void {
    // Do anything
  }
};

interface ChildClassConstructor {
  new (arg: number): ChildClass;
}

interface ChildClass {
  method(): void;
}

const ChildClass: ChildClassConstructor  = class extends ParentClass {
  constructor(number: number) {
    // Do something with argument
    super(number) // Only call `number`-based constructor of parent
  }
}

So I'm hoping this isn't an unreasonable request.

πŸ’» Use Cases / Examples

A feature like this would be incredibly helpful when it comes to maintenance of libraries using classes that require (or are improved by) well-defined generics. The best libraries give end-developers tools that give them the features they need without restricting their options, but these libraries often provide baked in solutions for common use cases. When these libraries depend on the concept of classes, there's often a base class -- with the baked-in solutions extending the base class for ease. This guarantees that some devs can use the baked in solution if it's sufficient, while others can reach for the more low-level option if needed.

I'm working on a frontend tool that follows the idea of an "Observer" (e.g., IntersectionObserver, MutationObserver, etc.). But it's written with JS and interacts with the DOM. This class (let's call it BaseObserver) has multiple constructor overloads to support different kinds of options for setting up the observer. And these constructors need to be generic to make it clear what combinations of constructor argument types work and what combinations don't work. (Unions alone won't make it clear what combinations are or are not legal.) In some cases, generic constructors are also needed for DOM-related type inference.

There are common use cases that this observer will be needed for. And rather than require the end-dev to come up with a solution on their own, I'm seeking to export other reliable, tested solutions as well. In the end, I'd like to have something like this:

/* -------------------- Base Observer -------------------- */
interface BaseObserverConstructor {
  new <T extends number>(arg: T): BaseObserver;
  new <T extends string>(arg: T): BaseObserver;
}

interface BaseObserver {
  observe(element: HTMLElement): void;
  unobserve(element: HTMLElement): void;
  disconnect(): void;
}

export const BaseObserver: BaseObserverConstructor = class<T extends number | string> {
  constructor(arg: T) {
    // Do something with argument
  }

  observe(element: HTMLElement): void {
    // Observe the element
  }

  unobserve(element: HTMLElement): void {
    // Undo the observation
  }

  disconnect(): void {
    // `unobserve` everything
  }
};

/* -------------------- Specific Observer -------------------- */
interface SpecificObserverConstructor {
  new <T extends number>(arg: T): SpecificObserver;
}

interface SpecificObserver extends BaseObserver {
  additionalNeededMethods(): void;
}

export const SpecificObserver: SpecificObserverConstructor = class<T extends number> extends BaseObserver<T> {
  constructor(arg: T) {
    // Do something with the argument

    super(arg); // ONLY call the `number`-based overload of `BaseObserver`
  }

  observe(element: HTMLElement): void {
    // Do any additional setup required
    super.observe(element);
  }

  unobserve(element: HTMLElement): void {
    // Do any additional teardown required
    super.unobserve(element);
  }

  additionalNeededMethods(): void {
    // Anything extra needed to guarantee this specific class works as needed
  }
};

Although I don't know ahead of time what the developer will do with BaseObserver, I do know what the exact shape and purpose of SpecificObserver will need to be. So I can make SpecificObserver conform to a subset of the generic types supported by BaseObserver and provide that to the end-devs. But to do that, I need the compiler's permission. πŸ˜…

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions