Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using mixins throws TS4093, TS4020 and TS4060 when exporting the class that extends the result of a mixin #15066

Closed
rolandjitsu opened this issue Apr 7, 2017 · 9 comments
Assignees
Labels
Bug A bug in TypeScript Fixed A PR has been merged for this issue

Comments

@rolandjitsu
Copy link

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.

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Apr 7, 2017

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.

@rolandjitsu
Copy link
Author

rolandjitsu commented Apr 7, 2017

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 {}

@HerringtonDarkholme
Copy link
Contributor

It seems TS either need to natively support mixin or support function call signature, #6606.

Otherwise it is quite hard to generate optimal declaration file.

@mhegazy
Copy link
Contributor

mhegazy commented Apr 24, 2017

This should be covered by #14075, but leaving it open to ensure we capture all the scenarios.

@hediet
Copy link
Member

hediet commented Apr 30, 2017

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.

@sandersn
Copy link
Member

A simple fix is up at #15932. It just emits class expressions as type literals, but it's enough to make this scenario work.

@ghost
Copy link

ghost commented Nov 19, 2017

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

@ghost
Copy link

ghost commented Nov 23, 2017

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.

@ghost
Copy link

ghost commented Dec 21, 2017

I've found that typescript has intersection types Type1 & Type2. I did not noticed it before. It is perfect for mixing.

@microsoft microsoft locked and limited conversation to collaborators Jun 21, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Bug A bug in TypeScript Fixed A PR has been merged for this issue
Projects
None yet
Development

No branches or pull requests

6 participants