microsoft / microsoft/TypeScript

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

Open
#52,585 9 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Awaiting More Feedback Suggestion
Dominant language
Go
Stars
111k
Forks
14.4k
Avg merge
1d 19h
Merged PRs (30d)
117

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the generic constructor-overload examples from the issue, including the TypeScript 4.9 behavior, then trace the compiler's checking of generic class heritage and overloaded construct signatures. Done means a child class constrained to a subset of the parent's generic constructor overloads is accepted without weakening existing incompatible cases, with regression tests covering both examples.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.