-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Mixin classes with private/protected constructors are subclassable #42264
Comments
I have found the cause of the bug. Updated Minimal Bug ReproductionThere'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: function ShouldThrowButDoesnt<TBase extends new(...args: any[]) => any>(Base: TBase) {
return class ShouldThrowButDoesnt extends Base {
private constructor(...args: any[]) {
super();
}
}
}
function ShouldThrowAndDoes(Base: new(...args: any[]) => any) {
return class ShouldThrowAndDoes extends Base {
private constructor(...args: any[]) {
super();
}
}
}
class Base {
constructor() {}
}
const shouldThrowButDoesnt = ShouldThrowButDoesnt(Base);
const shouldThrowAndDoes = ShouldThrowAndDoes(Base);
new shouldThrowButDoesnt();
new shouldThrowAndDoes(); Problem CauseThe only difference between the two mixins Now, the cause of the problem is in how TypeScript resolves this intersection. In function resolveIntersectionTypeMembers(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).
let callSignatures: Signature[] | undefined;
let constructSignatures: Signature[] | undefined;
let indexInfos: IndexInfo[] | undefined;
const types = type.types;
const mixinFlags = findMixins(types);
const mixinCount = countWhere(mixinFlags, (b) => b);
for (let i = 0; i < types.length; i++) {
const t = 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]) {
let signatures = getSignaturesOfType(t, SignatureKind.Construct);
if (signatures.length && mixinCount > 0) {
signatures = map(signatures, s => {
const clone = cloneSignature(s);
clone.resolvedReturnType = includeMixinType(getReturnTypeOfSignature(s), types, mixinFlags, i);
return clone;
});
}
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! However, I'm not really sure how a good solution would look like. Removing the |
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 declare function ShouldThrowButDoesent<TBase extends new (...args: any[]) => any>(Base: TBase): typeof class extends TBase {
private constructor(...args: any[]);
}; Unfortunately, there's still a lot of work to go on #41587, judging from the notes in #41824. |
Bug Report
🔎 Search Terms
mixin, private constructor
🕗 Version & Regression Information
This is the behavior in every version I tried, and I reviewed the FAQ for entries about Classes.
Versions tried:
⏯ Playground Link
Playground link with relevant code
💻 Code
🙁 Actual behavior
NonSubclassableThroughExtension
correctly throw an error — I've extended a superclassBase
, which has a publicly accessible constructor, and provided a private constructor in the derived class. I can't access the derived class' constructor anymore:The
PreventSubclassingMixin
supposedly works the same — it creates a class expression that extends theBase
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 theBase
:const NonSubclassableThroughMixin = PreventSubclassingMixin(Base)
, and try to initialize it, Typescript won't throw any error.🙂 Expected behavior
new NonSubclassableThroughMixin()
should throw aConstructor of class 'NonSubclassableThroughExtension' is private and only accessible within the class declaration.(2673)
.The text was updated successfully, but these errors were encountered: