-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Generating type definitions for mixin classes with protected members #17744
Comments
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 |
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 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. |
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. OverviewThese are 3 approaches that can be used for writing mixins that need to expose protected methods. 1. Symbol approach
Pros
Cons
2. Dumb class approach
Pros
Cons
// with dumb class: confusing
<T extends Constructor<SlottedItemsClass>>(base: T)
// without dumb class: clean
<T extends Constructor<LitElement>>(base: T)
3. Public methods
Pros
Cons
SummaryPersonally, 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 #17293 (comment), 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? |
Still needs a proposal. |
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.
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
would be the same as the type of the
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. |
@weswigham I think a list of why it is needed is in this issue: #35822 |
I come up with a (maybe) visually better solution based on The Dumb Class Approach. Pros:
Cons:
export type ConstructorOf<T> = new (...args: unknown[]) => T
export type MixinFactory<T, TBase> = (Base: ConstructorOf<TBase>) => ConstructorOf<T>
export interface MixinOf<T> {
(): ConstructorOf<T>
<U = {}>(Base?: ConstructorOf<U>): ConstructorOf<T & U>
}
export function createMixin<TBase = {}, T = TBase>(factory: MixinFactory<T, TBase>): MixinOf<T & TBase> {
return ((Base = Object) => factory(Base as any)) as any
}
export type MixinType<T extends MixinOf<unknown>> = T extends MixinOf<infer U> ? U : never
// Case 1. define a normal mixin (with no type parameter)
export type FooMixin = MixinType<typeof FooMixin>
export const FooMixin = createMixin(Base => class FooMixin extends Base {
foo() {}
})
// Case 2. define a mixin with protected/private fields (with type parameter)
declare class BarMixinBase {
protected bar(): void
}
export type BarMixin = MixinType<typeof BarMixin>
export const BarMixin = createMixin<BarMixinBase>(Base => class BarMixin extends Base {
protected bar() {}
})
// Usage
export class MyClass extends FooMixin(BarMixin()) {
test() {
this.foo()
this.bar()
}
} Hope the TS team to eventually solve the issue. |
Because of typescript bug microsoft/TypeScript#17744 all protected and private members were made public and a comment added indicating whether they were previously private or public
TypeScript Version: 2.4.2
Code:
I'm using mixins as described by: #13743
If I compile this code with
"declaration": true
to get type definitions for my library, I get the following error:One solution is to add an interface...
...and have my mixin
function
have a return type ofConstructor<IUnsubscriber>
. This works, but it forces me to make the properties/methods exposed by my mixin bepublic
even in cases where I want them to beprotected
.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.The text was updated successfully, but these errors were encountered: