Description
Bug Report
When returning an extended class type from a function, the resulting .d.ts
file is needlessly verbose resulting in very, very long .d.ts
for chained extensions. (See playground for details).
🔎 Search Terms
emitDeclarations
polymorphic this
mixins
Base constructor must have same return type
Type instantiation is excessively deep and possibly infinite
🕗 Version & Regression Information
- Version v4.2.3 is the minimum version where the code can run without errors.
- All versions output this repeated structure
⏯ Playground Link
💻 Code
class CustomElement extends HTMLElement {
/**
* Useless constructor needed to workaround TS
* `Base constructors must all have the same return type.`
*/
constructor(...args: any) {
super();
}
/* Extends Class.prototype and returns new Class */
static define<
CLASS extends typeof CustomElement,
ARGS extends ConstructorParameters<CLASS>,
INSTANCE extends InstanceType<CLASS>,
PROPS extends object>
(this: CLASS, source: PROPS & ThisType<PROPS & INSTANCE>)
: CLASS & (new (...args: ARGS) => INSTANCE & PROPS) {
return Object.defineProperties(
this.prototype,
source as any,
) as any;
}
}
/** FooElement.js */
class FooElement extends CustomElement
.define({
/** Comment hover test */
definedInFoo1: true,
})
.define({
definedInFoo2: 'bar',
})
.define({
definedInFoo3: 3,
})
.define({
/** Sets 2 to 'foo' */
fooMethod() {
this.definedInFoo2 = 'foo';
}
}) { }
🙁 Actual behavior
Here's the outputed d.ts:
declare class CustomElement extends HTMLElement {
/**
* Useless constructor needed to workaround TS
* `Base constructors must all have the same return type.`
*/
constructor(...args: any);
static define<CLASS extends typeof CustomElement, ARGS extends ConstructorParameters<CLASS>, INSTANCE extends InstanceType<CLASS>, PROPS extends object>(this: CLASS, source: PROPS & ThisType<PROPS & INSTANCE>): CLASS & (new (...args: ARGS) => INSTANCE & PROPS);
}
declare const FooElement_base: typeof CustomElement & (new (...args: any) => CustomElement & {
/** Comment hover test */
definedInFoo1: boolean;
}) & (new (...args: any) => CustomElement & {
/** Comment hover test */
definedInFoo1: boolean;
} & {
definedInFoo2: string;
}) & (new (...args: any) => CustomElement & {
/** Comment hover test */
definedInFoo1: boolean;
} & {
definedInFoo2: string;
} & {
definedInFoo3: number;
}) & (new (...args: any) => CustomElement & {
/** Comment hover test */
definedInFoo1: boolean;
} & {
definedInFoo2: string;
} & {
definedInFoo3: number;
} & {
/** Sets 2 to 'foo' */
fooMethod(): void;
});
/** FooElement.js */
declare class FooElement extends FooElement_base {
}
🙂 Expected behavior
I expected FooElement_base
to be shorter, smaller d.ts
like this:
declare const FooElement_base: typeof CustomElement & (new (...args: any) => CustomElement & {
/** Comment hover test */
definedInFoo1: boolean;
definedInFoo2: string;
definedInFoo3: number;
/** Sets 2 to 'foo' */
fooMethod(): void;
});
It seems repeated structures aren't analyzed and flattened. I also don't see a way to flatten them myself. Changing CLASS
to {[P in keyof CLASS]: CLASS[P]}
will make each item reiterate every single property in HTMLElement
in the d.ts.
This is a short reproducible, but actual code can get really heavy especially with mixins. I believe (but can't confirm), it causes the "Type instantiation is excessively deep and possibly infinite" error as well. There's probably a performance gain somewhere here as well. I know TS
will flatten types, but not constructor return arguments. Maybe, simplified that's the core issue.
I have a second playground link that has a more verbose example including mixin patterns and extending classes extending classes:
Related: