Skip to content

Commit ab81536

Browse files
mattmccutchenweswigham
authored andcommitted
Fix mixin logic to preserve at least one constructor type even when the (#27701)
intersection also contains non-constructor types. Fixes #17388.
1 parent 6c6f216 commit ab81536

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
@@ -7026,13 +7026,23 @@ namespace ts {
70267026
getUnionType([info1.type, info2.type]), info1.isReadonly || info2.isReadonly);
70277027
}
70287028

7029-
function includeMixinType(type: Type, types: ReadonlyArray<Type>, index: number): Type {
7029+
function findMixins(types: ReadonlyArray<Type>): ReadonlyArray<boolean> {
7030+
const constructorTypeCount = countWhere(types, (t) => getSignaturesOfType(t, SignatureKind.Construct).length > 0);
7031+
const mixinFlags = map(types, isMixinConstructorType);
7032+
if (constructorTypeCount > 0 && constructorTypeCount === countWhere(mixinFlags, (b) => b)) {
7033+
const firstMixinIndex = mixinFlags.indexOf(/*searchElement*/ true);
7034+
mixinFlags[firstMixinIndex] = false;
7035+
}
7036+
return mixinFlags;
7037+
}
7038+
7039+
function includeMixinType(type: Type, types: ReadonlyArray<Type>, mixinFlags: ReadonlyArray<boolean>, index: number): Type {
70307040
const mixedTypes: Type[] = [];
70317041
for (let i = 0; i < types.length; i++) {
70327042
if (i === index) {
70337043
mixedTypes.push(type);
70347044
}
7035-
else if (isMixinConstructorType(types[i])) {
7045+
else if (mixinFlags[i]) {
70367046
mixedTypes.push(getReturnTypeOfSignature(getSignaturesOfType(types[i], SignatureKind.Construct)[0]));
70377047
}
70387048
}
@@ -7047,20 +7057,21 @@ namespace ts {
70477057
let stringIndexInfo: IndexInfo | undefined;
70487058
let numberIndexInfo: IndexInfo | undefined;
70497059
const types = type.types;
7050-
const mixinCount = countWhere(types, isMixinConstructorType);
7060+
const mixinFlags = findMixins(types);
7061+
const mixinCount = countWhere(mixinFlags, (b) => b);
70517062
for (let i = 0; i < types.length; i++) {
70527063
const t = type.types[i];
70537064
// When an intersection type contains mixin constructor types, the construct signatures from
70547065
// those types are discarded and their return types are mixed into the return types of all
70557066
// other construct signatures in the intersection type. For example, the intersection type
70567067
// '{ new(...args: any[]) => A } & { new(s: string) => B }' has a single construct signature
70577068
// 'new(s: string) => A & B'.
7058-
if (mixinCount === 0 || mixinCount === types.length && i === 0 || !isMixinConstructorType(t)) {
7069+
if (!mixinFlags[i]) {
70597070
let signatures = getSignaturesOfType(t, SignatureKind.Construct);
70607071
if (signatures.length && mixinCount > 0) {
70617072
signatures = map(signatures, s => {
70627073
const clone = cloneSignature(s);
7063-
clone.resolvedReturnType = includeMixinType(getReturnTypeOfSignature(s), types, i);
7074+
clone.resolvedReturnType = includeMixinType(getReturnTypeOfSignature(s), types, mixinFlags, i);
70647075
return clone;
70657076
});
70667077
}
@@ -21104,12 +21115,11 @@ namespace ts {
2110421115
const firstBase = baseTypes[0];
2110521116
if (firstBase.flags & TypeFlags.Intersection) {
2110621117
const types = (firstBase as IntersectionType).types;
21107-
const mixinCount = countWhere(types, isMixinConstructorType);
21118+
const mixinFlags = findMixins(types);
2110821119
let i = 0;
2110921120
for (const intersectionMember of (firstBase as IntersectionType).types) {
21110-
i++;
2111121121
// We want to ignore mixin ctors
21112-
if (mixinCount === 0 || mixinCount === types.length && i === 0 || !isMixinConstructorType(intersectionMember)) {
21122+
if (!mixinFlags[i]) {
2111321123
if (getObjectFlags(intersectionMember) & (ObjectFlags.Class | ObjectFlags.Interface)) {
2111421124
if (intersectionMember.symbol === target) {
2111521125
return true;
@@ -21119,6 +21129,7 @@ namespace ts {
2111921129
}
2112021130
}
2112121131
}
21132+
i++;
2112221133
}
2112321134
return false;
2112421135
}
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)