You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Mixin classes with private/protected constructors are subclassable #42264
functionPreventSubclassingMixin<TBaseextendsnew(...args: any[])=>any>(Base: TBase){returnclassNonSubclassableextendsBase{privateconstructor(...args: any[]){super();}}}classBase{constructor(){}}classNonSubclassableThroughExtensionextendsBase{privateconstructor(){super();}}constNonSubclassableThroughMixin=PreventSubclassingMixin(Base);newNonSubclassableThroughExtension();// Constructor of class 'NonSubclassableThroughExtension' is private and only accessible within the class declaration.(2673)newNonSubclassableThroughMixin();// Doesn't throw an error
🙁 Actual behavior
NonSubclassableThroughExtension correctly throw an error — I've extended a superclass Base, which has a publicly accessible constructor, and provided a private constructor in the derived class. I can't access the derived class' constructor anymore:
newNonSubclassableThroughExtension();// Constructor of class 'NonSubclassableThroughExtension' is private and only accessible within the class declaration.(2673)
The PreventSubclassingMixin supposedly works the same — it creates a class expression that extends the Base class with a public constructor and defines a private constructor, and returns it. The returned derived mixin class is supposedly identical to the example above. However, if I apply the mixin to the Base: const NonSubclassableThroughMixin = PreventSubclassingMixin(Base), and try to initialize it, Typescript won't throw any error.
🙂 Expected behavior
new NonSubclassableThroughMixin() should throw a Constructor of class 'NonSubclassableThroughExtension' is private and only accessible within the class declaration.(2673).
There's no difference between subclassing through mixins vs subclassing through class extension, as I thought at the time of writing the bug report. To show this, here is an updated minimal bug reproduction, featuring two mixins:
The only difference between the two mixins ShouldThrowButDoesnt and ShouldThrowAndDoes is that in the former, the class parameter is of a generic type. Because of this (and I don't know exactly why), in checker.ts > resolveNewExpression(), when obtaining constructor signatures (checker.ts, line 29642 in v4.4.2), ShouldThrowButDoesnt is represented as an intersection type with type members ShouldThrowButDoesnt and Base (this intersection is obtained by getReducedApparentType()).
Now, the cause of the problem is in how TypeScript resolves this intersection. In checker.ts > resolveIntersectionTypeMembers()(emphasis mine):
functionresolveIntersectionTypeMembers(type: IntersectionType){// The members and properties collections are empty for intersection types. To get all properties of an// intersection type use getPropertiesOfType (only the language service uses this).letcallSignatures: Signature[]|undefined;letconstructSignatures: Signature[]|undefined;letindexInfos: IndexInfo[]|undefined;consttypes=type.types;constmixinFlags=findMixins(types);constmixinCount=countWhere(mixinFlags,(b)=>b);for(leti=0;i<types.length;i++){constt=type.types[i];// When an intersection type contains mixin constructor types, the construct signatures from// ^^^^^^^^^^^^. ^^^^^^^^^^^^^^^^^// those types are discarded and their return types are mixed into the return types of all// ^^^^^^^^^// other construct signatures in the intersection type. For example, the intersection type// '{ new(...args: any[]) => A } & { new(s: string) => B }' has a single construct signature// 'new(s: string) => A & B'.if(!mixinFlags[i]){letsignatures=getSignaturesOfType(t,SignatureKind.Construct);if(signatures.length&&mixinCount>0){signatures=map(signatures,s=>{constclone=cloneSignature(s);clone.resolvedReturnType=includeMixinType(getReturnTypeOfSignature(s),types,mixinFlags,i);returnclone;});}constructSignatures=appendSignatures(constructSignatures,signatures);}callSignatures=appendSignatures(callSignatures,getSignaturesOfType(t,SignatureKind.Call));indexInfos=reduceLeft(getIndexInfosOfType(t),(infos,newInfo)=>appendIndexInfo(infos,newInfo,/*union*/false),indexInfos);}setStructuredTypeMembers(type,emptySymbols,callSignatures||emptyArray,constructSignatures||emptyArray,indexInfos||emptyArray);}
Paraphrasing what was said in the comments, the mixin constructor signatures are discarded and instead their return types are merged (because mixins are not supposed to mess with a class' constructor signature).
Therefore, mixin constructor's accessibility isn't taken into consideration at all! Ryan Cavanaugh (@RyanCavanaugh)Andrew Branch (@andrewbranch) I'd appreciate it if you could review this update and assign the bug to me (I'm sorry if it's not appropriate to tag you directly).
However, I'm not really sure how a good solution would look like. Removing the !mixinFlags[i] check makes sense, but breaks a bunch of different tests. Perhaps we should only discard mixins that have a public constructor, but process all those that have a non-public one?
We don’t assign bugs to community members. Any bug in the Backlog milestone is fair game for any community member to submit a PR, but there is no guarantee we will accept it, and your chances of getting a review and the support you need is much better if you choose an issue labeled “help wanted.” As it is, we can’t take the time to investigate this bug and answer your questions about it right now because it’s on the backlog, and we have work that is milestoned for upcoming releases that needs our attention. If you want to contribute, I would strongly recommend looking at the “help wanted” label first.
This seems like something that #41587 would solve, since we can't annotate an interface or function/constructor type with visibility modifiers like private/protected. With #41587, we could change the inferred return type of ShouldThrowButDoesnt to use a typeof class so that this works both in the current project and when imported from a declaration file:
Activity
rafasofizada commentedon Sep 7, 2021
I have found the cause of the bug.
Updated Minimal Bug Reproduction
There's no difference between subclassing through mixins vs subclassing through class extension, as I thought at the time of writing the bug report. To show this, here is an updated minimal bug reproduction, featuring two mixins:
Problem Cause
The only difference between the two mixins
ShouldThrowButDoesntandShouldThrowAndDoesis that in the former, the class parameter is of a generic type. Because of this (and I don't know exactly why), inchecker.ts > resolveNewExpression(), when obtaining constructor signatures (checker.ts, line 29642 in v4.4.2),ShouldThrowButDoesntis represented as an intersection type with type membersShouldThrowButDoesntandBase(this intersection is obtained bygetReducedApparentType()).Now, the cause of the problem is in how TypeScript resolves this intersection. In
checker.ts > resolveIntersectionTypeMembers()(emphasis mine):Paraphrasing what was said in the comments, the mixin constructor signatures are discarded and instead their return types are merged (because mixins are not supposed to mess with a class' constructor signature).
Therefore, mixin constructor's accessibility isn't taken into consideration at all!
Ryan Cavanaugh (@RyanCavanaugh) Andrew Branch (@andrewbranch) I'd appreciate it if you could review this update and assign the bug to me (I'm sorry if it's not appropriate to tag you directly).
However, I'm not really sure how a good solution would look like. Removing the
!mixinFlags[i]check makes sense, but breaks a bunch of different tests. Perhaps we should only discard mixins that have a public constructor, but process all those that have a non-public one?andrewbranch commentedon Sep 7, 2021
We don’t assign bugs to community members. Any bug in the Backlog milestone is fair game for any community member to submit a PR, but there is no guarantee we will accept it, and your chances of getting a review and the support you need is much better if you choose an issue labeled “help wanted.” As it is, we can’t take the time to investigate this bug and answer your questions about it right now because it’s on the backlog, and we have work that is milestoned for upcoming releases that needs our attention. If you want to contribute, I would strongly recommend looking at the “help wanted” label first.
rbuckton commentedon Oct 5, 2021
This seems like something that #41587 would solve, since we can't annotate an interface or function/constructor type with visibility modifiers like
private/protected. With #41587, we could change the inferred return type ofShouldThrowButDoesntto use atypeof classso that this works both in the current project and when imported from a declaration file:Unfortunately, there's still a lot of work to go on #41587, judging from the notes in #41824.