Skip to content

Commit a8b6164

Browse files
committed
Fix mixin logic to preserve at least one constructor type even when the
intersection also contains non-constructor types. Fixes microsoft#17388.
1 parent b2bae85 commit a8b6164

5 files changed

+56
-8
lines changed

src/compiler/checker.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6639,13 +6639,23 @@ namespace ts {
66396639
getUnionType([info1.type, info2.type]), info1.isReadonly || info2.isReadonly);
66406640
}
66416641

6642-
function includeMixinType(type: Type, types: ReadonlyArray<Type>, index: number): Type {
6642+
function findMixins(types: ReadonlyArray<Type>): ReadonlyArray<boolean> {
6643+
const constructorTypeCount = countWhere(types, (t) => getSignaturesOfType(t, SignatureKind.Construct).length > 0);
6644+
const mixinFlags = map(types, isMixinConstructorType);
6645+
if (constructorTypeCount > 0 && constructorTypeCount === countWhere(mixinFlags, (b) => b)) {
6646+
const firstMixinIndex = mixinFlags.indexOf(/*searchElement*/ true);
6647+
mixinFlags[firstMixinIndex] = false;
6648+
}
6649+
return mixinFlags;
6650+
}
6651+
6652+
function includeMixinType(type: Type, types: ReadonlyArray<Type>, mixinFlags: ReadonlyArray<boolean>, index: number): Type {
66436653
const mixedTypes: Type[] = [];
66446654
for (let i = 0; i < types.length; i++) {
66456655
if (i === index) {
66466656
mixedTypes.push(type);
66476657
}
6648-
else if (isMixinConstructorType(types[i])) {
6658+
else if (mixinFlags[i]) {
66496659
mixedTypes.push(getReturnTypeOfSignature(getSignaturesOfType(types[i], SignatureKind.Construct)[0]));
66506660
}
66516661
}
@@ -6660,20 +6670,21 @@ namespace ts {
66606670
let stringIndexInfo: IndexInfo | undefined;
66616671
let numberIndexInfo: IndexInfo | undefined;
66626672
const types = type.types;
6663-
const mixinCount = countWhere(types, isMixinConstructorType);
6673+
const mixinFlags = findMixins(types);
6674+
const mixinCount = countWhere(mixinFlags, (b) => b);
66646675
for (let i = 0; i < types.length; i++) {
66656676
const t = type.types[i];
66666677
// When an intersection type contains mixin constructor types, the construct signatures from
66676678
// those types are discarded and their return types are mixed into the return types of all
66686679
// other construct signatures in the intersection type. For example, the intersection type
66696680
// '{ new(...args: any[]) => A } & { new(s: string) => B }' has a single construct signature
66706681
// 'new(s: string) => A & B'.
6671-
if (mixinCount === 0 || mixinCount === types.length && i === 0 || !isMixinConstructorType(t)) {
6682+
if (!mixinFlags[i]) {
66726683
let signatures = getSignaturesOfType(t, SignatureKind.Construct);
66736684
if (signatures.length && mixinCount > 0) {
66746685
signatures = map(signatures, s => {
66756686
const clone = cloneSignature(s);
6676-
clone.resolvedReturnType = includeMixinType(getReturnTypeOfSignature(s), types, i);
6687+
clone.resolvedReturnType = includeMixinType(getReturnTypeOfSignature(s), types, mixinFlags, i);
66776688
return clone;
66786689
});
66796690
}
@@ -20169,12 +20180,11 @@ namespace ts {
2016920180
const firstBase = baseTypes[0];
2017020181
if (firstBase.flags & TypeFlags.Intersection) {
2017120182
const types = (firstBase as IntersectionType).types;
20172-
const mixinCount = countWhere(types, isMixinConstructorType);
20183+
const mixinFlags = findMixins(types);
2017320184
let i = 0;
2017420185
for (const intersectionMember of (firstBase as IntersectionType).types) {
20175-
i++;
2017620186
// We want to ignore mixin ctors
20177-
if (mixinCount === 0 || mixinCount === types.length && i === 0 || !isMixinConstructorType(intersectionMember)) {
20187+
if (!mixinFlags[i]) {
2017820188
if (getObjectFlags(intersectionMember) & (ObjectFlags.Class | ObjectFlags.Interface)) {
2017920189
if (intersectionMember.symbol === target) {
2018020190
return true;
@@ -20184,6 +20194,7 @@ namespace ts {
2018420194
}
2018520195
}
2018620196
}
20197+
i++;
2018720198
}
2018820199
return false;
2018920200
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//// [intersectionOfMixinConstructorTypeAndNonConstructorType.ts]
2+
// Repro for #17388
3+
4+
declare let x: {foo: undefined} & {new(...args: any[]): any};
5+
new x();
6+
7+
8+
//// [intersectionOfMixinConstructorTypeAndNonConstructorType.js]
9+
// Repro for #17388
10+
new x();
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
=== tests/cases/compiler/intersectionOfMixinConstructorTypeAndNonConstructorType.ts ===
2+
// Repro for #17388
3+
4+
declare let x: {foo: undefined} & {new(...args: any[]): any};
5+
>x : Symbol(x, Decl(intersectionOfMixinConstructorTypeAndNonConstructorType.ts, 2, 11))
6+
>foo : Symbol(foo, Decl(intersectionOfMixinConstructorTypeAndNonConstructorType.ts, 2, 16))
7+
>args : Symbol(args, Decl(intersectionOfMixinConstructorTypeAndNonConstructorType.ts, 2, 39))
8+
9+
new x();
10+
>x : Symbol(x, Decl(intersectionOfMixinConstructorTypeAndNonConstructorType.ts, 2, 11))
11+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
=== tests/cases/compiler/intersectionOfMixinConstructorTypeAndNonConstructorType.ts ===
2+
// Repro for #17388
3+
4+
declare let x: {foo: undefined} & {new(...args: any[]): any};
5+
>x : { foo: undefined; } & (new (...args: any[]) => any)
6+
>foo : undefined
7+
>args : any[]
8+
9+
new x();
10+
>new x() : any
11+
>x : { foo: undefined; } & (new (...args: any[]) => any)
12+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Repro for #17388
2+
3+
declare let x: {foo: undefined} & {new(...args: any[]): any};
4+
new x();

0 commit comments

Comments
 (0)