Closed
Description
Bug Report
π Search Terms
mixin declaration
π Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about v4.2.0-dev.20210207
β― Playground Link
Playground link with relevant code
π» Code
// @showEmit
// @showEmittedFile: MyPanel.d.ts
// @declaration
// @filename: singleton.ts
type Constructor<T = {}> = new (...args: any[]) => T;
export function singleton<TBase extends Constructor>(baseClass: TBase) {
let instance: InstanceType<TBase>;
return class extends baseClass {
static instance(opts: {forceNew?: boolean} = {forceNew: undefined}): InstanceType<TBase> {
const {forceNew} = opts;
if (!instance || forceNew) {
instance = new this() as InstanceType<TBase>;
}
return instance;
}
}
}
// @declaration
// @filename: MyWidget.ts
export class MyWidget {
public publicMethod(): void {
this.someMethod();
}
public someMethod(): void {
}
}
// @declaration
// @filename: MyPanel.ts
import {singleton} from './singleton.js';
import {MyWidget} from './MyWidget.js';
// The `.d.ts` file of `MyPanel.ts` includes a new definition of a MyPanel_Base
export class MyPanel extends singleton(class extends MyWidget {}) {}
// A manual declaration of the base class will work as expected
class MySecondPanelBase extends MyWidget {}
export class MySecondPanel extends singleton(MySecondPanelBase) {}
π Actual behavior
The .d.ts
file for MyPanel
includes a full copy of the anonymous class, including its base class. In other words: anything defined in a base class of an anonymous class that is mixed-in gets copied into the .d.ts
file. A manual class declaration which gets passed into a mixin declaration does not show the same behavior.
declare const MyPanel_base: {
new (...args: any[]): {};
instance(opts?: {
forceNew?: boolean | undefined;
}): {
publicMethod(): void;
someMethod(): void;
};
} & {
new (): {
publicMethod(): void;
someMethod(): void;
};
};
export declare class MyPanel extends MyPanel_base {
}
export {};
π Expected behavior
Mixing an anonymous class with a base class should generate the same .d.ts
with regards to how the base class is mixed-in. In the playground link, that would mean that MyPanel_base
does not copy the two methods defined on MyWidget
and instead extends MyWidget
.
declare class MyPanel_base extends MyWidget {
new (...args: any[]): {};
instance(opts?: {
forceNew?: boolean | undefined;
}): MyPanel_base;
} & typeof MyPanel_base;
export declare class MyPanel extends MyPanel_base {
}
export {};
Metadata
Metadata
Assignees
Labels
Type
Projects
Relationships
Development
No branches or pull requests
Activity
TimvdLippe commentedon Feb 10, 2021
For context, the CL where I ran into this problem is https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2687507/1
weswigham commentedon May 24, 2021
The instance type for your anonymous base class isn't accessible, so it's serialized structurally.
class extends MyWidget {}
is making a new anonymous subtype ofMyWidget
whose type is distinct fromMyWidget
- that anonymous class is then subtyped by thesingleton
function into another class via the mixin pattern. Since it's being used as an anonymous expression, there's no way to refer directly to (and thus, reuse) the instance type the class expression being passed tosingleton
makes. We're essentially doing the best we can without either #41587 or the kind of synthetic hoisting and naming described in #44045.TimvdLippe commentedon May 24, 2021
#41587 is very interesting and might also solve the
singleton
decorator type problem (e.g. remove the need foras InstanceType<TBase>;
)Thanks for investigating!