TypeScript Version: 2.2.1
Code
I'm having some problems with using the new syntax for mixins. Consider the following example:
export abstract class Item {
foo(): void {}
}
export class FooItem extends Item {
name?: string;
toString(): string {
if (this.name) {
return this.name;
}
return `${this.id}`;
}
}
export type Constructor<T> = new(...args: any[]) => T;
// Throws an error when compiled
// "Error TS4060: Return type of exported function has or is using private name '(Anonymous class)'.
export function WithTags<T extends Constructor<FooItem>>(Base: T) {
return class extends Base {
static getTags(): Promise<any> { ... }
tags(): Promise<any> { ... }
}
}
// Throws two errors:
// At `Test`: "Error TS4093: 'extends' clause of exported class 'Test' refers to a type whose name cannot be referenced."
// At `WithTags`: "Error TS4020: 'extends' clause of exported class 'Test' has or is using private name '(Anonymous class)'."
export class Test extends WithTags(FooItem) {}
const test = new Test();
Test.getTags()
test.tags();
Expected behavior:
It should not throw any errors.
Actual behavior:
It throws those three errors that are described in the code comments.
This is still a bit painful. I was talking to @alexeagle and others about this today.
You can get around this by creating an explicit interface for your inner WithTags type. Then writing out the explicit return type for WithTags. For example:
export class FooItem {
name?: string;
toString(): string {
return ""
}
}
// Instance side:
export interface WithTagsInstance {
tags(): Promise<any>
}
// Constructor static side:
export interface WithTagsStatic {
getTags(): Promise<any>;
new (...args: any[]): WithTagsInstance;
}
export function WithTags<T extends Constructor<FooItem>>(Base: T): WithTagsStatic & T {
return class extends Base {
static getTags(): Promise<any> { throw 1; }
tags(): Promise<any> { throw 1; }
}
}
Then later on, you unfortunately cannot immediately extend WithTags(Foo). You have to use
export const FooItemWithTags = WithTags(FooItem)
export class Test extends FooItemWithTags {}
So that there's a direct name that TypeScript can reference.
I see. Though, if my Test is located in another file I get:
Error TS4023: Exported variable 'Test' has or is using name 'WithTagsStatic' from external module "... path to where WithTagsStatic is" but cannot be named.
Furthermore, It also complains about the .toString() method if Test overrides it:
export class Test extends WithTags(FooItem) {
// Error TS2425: Class 'WithTagsInstance & FooItem' defines instance member property 'toString', but extended class 'Test' defines it as instance member function.
toString(): string {
return '';
}
}
I'm also curious how would I implement the mixin if FooItem would also be an abstract class:
export abstract class Item {
foo(): void {}
}
export abstract class FooItem extends Item {
name?: string;
toString(): string {
if (this.name) {
return this.name;
}
return `${this.id}`;
}
}
...
// This definitely doesn't work ...
export const FooItemWithTags = WithTags(FooItem)
export class Test extends FooItemWithTags {}
It seems TS either need to natively support mixin or support function call signature, https://github.com/Microsoft/TypeScript/issues/6606.
Otherwise it is quite hard to generate optimal declaration file.
This should be covered by https://github.com/Microsoft/TypeScript/issues/14075, but leaving it open to ensure we capture all the scenarios.
How to deal with protected members in Mixins then?
@DanielRosenwasser's workaround does not work if one wants tags to be protected, as protected members cannot be declared in interfaces.
A simple fix is up at #15932. It just emits class expressions as type literals, but it's enough to make this scenario work.
Thanks very much. I used it with typescript version 2.6.1. It works like a charm. Even allows to extend later the mixed classes. That's how i used it in my project: see the MixedBaseObject function there https://github.com/BBGONE/JRIApp/blob/master2/FRAMEWORK/CLIENT/shared/jriapp_shared/utils/mixobj.ts
Yes, it works, but the problem that i need to have a constructor. This mixing works only on classes. EcmaScript6 now has a Proxy object, which can be used to add properties, methods to a given object, thus adding members to the interface. I can not use this mixing function in this case. Types union does not work either, because T | U type is not the same as T extends U.
I've found that typescript has intersection types Type1 & Type2. I did not noticed it before. It is perfect for mixing.
Most helpful comment
A simple fix is up at #15932. It just emits class expressions as type literals, but it's enough to make this scenario work.