TypeScript Version: 2.4.2
Code:
I'm using mixins as described by: https://github.com/Microsoft/TypeScript/pull/13743
export type Constructor<T> = new(...args: any[]) => T;
export function Unsubscriber<T extends Constructor<{}>>(Base: T) {
class Unsubscriber extends Base implements OnDestroy {
protected unsubscribe: Subject<void> = new Subject();
ngOnDestroy() {
this.unsubscribe.next();
this.unsubscribe.complete();
}
}
return Unsubscriber;
}
If I compile this code with "declaration": true
to get type definitions for my library, I get the following error:
ERROR in (truncated)/mixins.ts (...): Return type of exported function has or is using private name 'Unsubscriber'.
One solution is to add an interface...
export interface IUnsubscriber extends OnDestroy {
unsubscribe: Subject<void>;
}
...and have my mixin function
have a return type of Constructor<IUnsubscriber>
. This works, but it forces me to make the properties/methods exposed by my mixin be public
even in cases where I want them to be protected
.
Short of adding protected
members to interfaces (which I'm not sure is the right thing to do), this seems to be a limitation of the currently supported mixin strategy.
BTW I found that you can work around this limitation to some extent by using ECMAScript symbols, which provide a non-TypeScript way to hide a member:
export type Constructor<T> = new(...args: any[]) => T;
const unsubscribe: unique symbol = Symbol('Unsubscriber.unsubscribe');
export function Unsubscriber<T extends Constructor<{}>>(Base: T) {
class Unsubscriber extends Base implements OnDestroy {
public [unsubscribe]: Subject<void> = new Subject();
ngOnDestroy() {
this[unsubscribe].next();
this[unsubscribe].complete();
}
}
return Unsubscriber;
}
IntelliSense will only show the unsubscribe
member to callers who have access to the unsubscribe symbol.
It bothers me too. Seems that this issue was moved into future milestone. No plan to solve it in the near future?
BTW I recently came up with an alternate way to represent mixins in TypeScript. Consider this example:
// Helper that copies properties from base's prototype
// to child's prototype
function extend(child: any, base: any): void {
for (var property in base) {
var descriptor = Object.getOwnPropertyDescriptor(base, property) || { value: base[property] };
Object.defineProperty(child, property, descriptor);
}
}
// The mixin base classes
class Base1 {
constructor() {
console.log('Base1.constructor() called')
}
public method1(): void {
console.log('method1() called')
}
}
class Base2 {
constructor() {
console.log('Base2.constructor() called')
}
public method2(): void {
console.log('method2() called')
}
}
class Base3 {
constructor() {
console.log('Base3.constructor() called')
}
private _prop3 = 123;
public get prop3(): number {
return this._prop3;
}
}
// Collect the base classes into an intermediary "Mixin" class
type Mixin = Base1 & Base2 & Base3;
const Mixin: { new(): Mixin } = class {
constructor() {
Base1.call(this);
Base2.call(this);
Base3.call(this);
}
} as any;
extend(Mixin.prototype, Base1.prototype);
extend(Mixin.prototype, Base2.prototype);
extend(Mixin.prototype, Base3.prototype);
// Extend from the "Mixin" class
class Child extends Mixin {
public method4(): void {
console.log('method4() called')
}
}
const child = new Child();
child.method1();
child.method2();
console.log('prop3 = ' + child.prop3);
child.method4();
The behavior is probably unsound if the base classes happen to have members with the same name. But this issue could be detected in the extend()
function.
There are some interesting advantages:
Not sure I'd recommend to do this in real code, but it's interesting to contrast with the class-expression approach to mixins.
It bothers me too. Seems that this issue was moved into future milestone. No plan to solve it in the near future?
Well me too !
@RyanCavanaugh Any update on when this could be fixed ? It's set on the "Future" backlog but as it's pretty huge, could you add some precision ?
Here is my research regarding possible workarounds for TS mixins with protected methods.
These are 3 approaches that can be used for writing mixins that need to expose protected methods.
They all allow subclasses extending such mixins to call these methods using super
.
Not using protected
methods in the mixins
Using Symbol
as recommended by Elix
Example: see above comment https://github.com/microsoft/TypeScript/issues/17744#issuecomment-431534647
Methods are truly protected (can't be called from outside, e.g. by third party code)
Methods are "public" in terms of TS and can be annotated on the interface
Explicit exports potentially help with refactoring of the consumers' subclasses
Methods are excluded from the DevTools console's auto-complete list (good for user)
Avoid potential name conflicts if user extends a class with their own methods
Consumers have to import symbols if they need to override protected methods
Jump to definition is not ideal (jumps to the place where Symbol()
is created)
Using protected
methods in the mixins
Using abstract class SomeMixinClass
to "annotate" them
Example: https://github.com/microsoft/TypeScript/issues/25163#issuecomment-507074489
No need to import anything (except the mixin itself) for the consumer component
Methods are easy to access in the DevTools console (good for maintainer)
Both interface (for public API) and abstract class (for protected API) are needed
The abstract class ends up in the JS output (as empty class with no methods)
The mixin function needs to confusingly accept the argument with a type cast (instead of the actual expected class) in order to pick up the annotated protected methods from it:
// with dumb class: confusing
<T extends Constructor<SlottedItemsClass>>(base: T)
// without dumb class: clean
<T extends Constructor<LitElement>>(base: T)
Need for extra super
check: super._focus && super._focus()
- this is needed because the abstract classes mark methods using _focus?()
, i.e. "possibly undefined"
Jump to definition from the subclass goes to a dumb class
Keep methods public
, add them on the interface
Jump to definition works as expected
TS users would get protected methods listed in the code completion
Messing up public
and protected
API might need tweaks for tools
Personally, I like symbols approach more because it's cleaner and bulletproof. The "jump to definition" inconvenience is a drawback that is more of a personal preference.
On the other hand, the dumb class approach has its benefits: once the issue is resolved, we can get rid of those dumb classes and potentially keep the methods themselves unchanged.
@weswigham according to your comment at https://github.com/microsoft/TypeScript/issues/17293#issuecomment-522491710, what do you have on your mind to tackle these issues? Is there a plan, or does this still need a concrete proposal that you mentioned?
Is there a plan, or does this still need a concrete proposal that you mentioned?
Still needs a proposal.
@fr0 Also worth noting https://github.com/microsoft/TypeScript/issues/35822
We hit this issue again when we tried to factor out a base class with private and protected members into a mixin.
Because we've already provided protected methods to subcasses of our base class, we really needed to keep existing protected members protected. If the members became public, then subclasses would get errors on trying to override public members with protected.
The workarounds we've had to do to satisfy the compiler are pretty onerous. We've created a fake class outside of the mixin and cast the mixin function to return an intersection of the argument and that class. Static require another fake class. Then we need to get our build system to remove the fake class. I'm not sure this approach solved every problem yet.
@weswigham
Still needs a proposal.
Is there anything anyone not on the TS team can do to help here?
Well, we discussed it at our last design meeting as future work beyond abstract constructor types, however some team members think allowing a class-expression-like-thing in a type position (and having that refer to the static shape of the declared class) would be confusing. IMO, it makes sense since the shape stored by
type A = class {}
would be the same as the type of the const
in
const A = class {}
But that's only my point of view. The disagreement within the team means we're going to need to see both a serious proposal and serious justification for why it's needed and how it makes sense before we're going to get close to agreement.
Most helpful comment
Here is my research regarding possible workarounds for TS mixins with protected methods.
Overview
These are 3 approaches that can be used for writing mixins that need to expose protected methods.
They all allow subclasses extending such mixins to call these methods using
super
.1. Symbol approach
Not using
protected
methods in the mixinsUsing
Symbol
as recommended by ElixExample: see above comment https://github.com/microsoft/TypeScript/issues/17744#issuecomment-431534647
Pros
Methods are truly protected (can't be called from outside, e.g. by third party code)
Methods are "public" in terms of TS and can be annotated on the
interface
Explicit exports potentially help with refactoring of the consumers' subclasses
Methods are excluded from the DevTools console's auto-complete list (good for user)
Avoid potential name conflicts if user extends a class with their own methods
Cons
Consumers have to import symbols if they need to override protected methods
Jump to definition is not ideal (jumps to the place where
Symbol()
is created)2. Dumb class approach
Using
protected
methods in the mixinsUsing
abstract class SomeMixinClass
to "annotate" themExample: https://github.com/microsoft/TypeScript/issues/25163#issuecomment-507074489
Pros
No need to import anything (except the mixin itself) for the consumer component
Methods are easy to access in the DevTools console (good for maintainer)
Cons
Both interface (for public API) and abstract class (for protected API) are needed
The abstract class ends up in the JS output (as empty class with no methods)
The mixin function needs to confusingly accept the argument with a type cast (instead of the actual expected class) in order to pick up the annotated protected methods from it:
Need for extra
super
check:super._focus && super._focus()
- this is needed because the abstract classes mark methods using_focus?()
, i.e. "possibly undefined"Jump to definition from the subclass goes to a dumb class
3. Public methods
Keep methods
public
, add them on theinterface
Jump to definition works as expected
Example: https://github.com/wikibus/Alcaeus/issues/33
Pros
Cons
TS users would get protected methods listed in the code completion
Messing up
public
andprotected
API might need tweaks for toolsSummary
Personally, I like symbols approach more because it's cleaner and bulletproof. The "jump to definition" inconvenience is a drawback that is more of a personal preference.
On the other hand, the dumb class approach has its benefits: once the issue is resolved, we can get rid of those dumb classes and potentially keep the methods themselves unchanged.
@weswigham according to your comment at https://github.com/microsoft/TypeScript/issues/17293#issuecomment-522491710, what do you have on your mind to tackle these issues? Is there a plan, or does this still need a concrete proposal that you mentioned?