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