Skip to content

Commit 8d6b841

Browse files
committed
Fix spread operator failing to distribute over union when type is inlined
Fixes #62812 When a union/intersection type like `number | string` appears in the true branch of a conditional type that has the same type as its check type, the type was incorrectly being narrowed with a substitution constraint. This caused issues when the union type was used as a type argument to a generic type - different occurrences of `number | string` in the code were being treated as the same entity when they should be independent. The fix adds a check to skip this narrowing for structural types (unions/intersections) that don't contain type variables. Named types (interfaces, classes, etc.) still get narrowed since different references to the same named type refer to the same entity.
1 parent db3ae1b commit 8d6b841

File tree

5 files changed

+436
-2
lines changed

5 files changed

+436
-2
lines changed

src/compiler/checker.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17200,8 +17200,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1720017200
covariant = !covariant;
1720117201
}
1720217202
// Always substitute on type parameters, regardless of variance, since even
17203-
// in contravariant positions, they may rely on substituted constraints to be valid
17204-
if ((covariant || type.flags & TypeFlags.TypeVariable) && parent.kind === SyntaxKind.ConditionalType && node === (parent as ConditionalTypeNode).trueType) {
17203+
// in contravariant positions, they may rely on substituted constraints to be valid.
17204+
// For union/intersection types that don't contain type variables, don't apply narrowing
17205+
// based on structural equality with the check type - different occurrences of
17206+
// `number | string` in the code are independent even if they resolve to the same
17207+
// canonical type. Named types (interfaces, etc.) should still be narrowed since
17208+
// different references to the same named type refer to the same entity.
17209+
const isStructuralTypeWithoutTypeVariables = !!(type.flags & TypeFlags.UnionOrIntersection) && !couldContainTypeVariables(type);
17210+
if (!isStructuralTypeWithoutTypeVariables && (covariant || type.flags & TypeFlags.TypeVariable) && parent.kind === SyntaxKind.ConditionalType && node === (parent as ConditionalTypeNode).trueType) {
1720517211
const constraint = getImpliedConstraint(type, (parent as ConditionalTypeNode).checkType, (parent as ConditionalTypeNode).extendsType);
1720617212
if (constraint) {
1720717213
constraints = append(constraints, constraint);
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//// [tests/cases/conformance/types/spread/spreadTupleUnionDistribution.ts] ////
2+
3+
//// [spreadTupleUnionDistribution.ts]
4+
// Repro from #62812
5+
// Spread operator fails to distribute over union when recursive type call is inlined instead of aliased
6+
7+
type CrossProduct<Union, Counter extends unknown[]> =
8+
Counter extends [infer Zero, ...infer Rest]
9+
? (Union extends infer Member
10+
? [Member, ...CrossProduct<Union, Rest>]
11+
: never)
12+
: [];
13+
14+
// Basic test - this works
15+
let test1: CrossProduct<number | string, [undefined]>; // [string] | [number]
16+
type Depth1 = CrossProduct<number | string, [undefined]> // [string] | [number]
17+
18+
// With alias - this should work and give full cross product
19+
let test2: (number | string extends infer Union ? (Union extends unknown ? [Union, ...Depth1]: never) : never);
20+
// Expected: [string, string] | [number, number] | [string, number] | [number, string]
21+
22+
// With inlined type - this should also work but currently doesn't distribute properly
23+
let test3: (number | string extends infer Union ? (Union extends unknown ? [Union, ...CrossProduct<number | string, [undefined]>]: never) : never);
24+
// Expected: [string, string] | [number, number] | [string, number] | [number, string]
25+
// Actual (bug): [string, string] | [number, number]
26+
27+
// With literal union - this works
28+
let test4: (number | string extends infer Union ? (Union extends unknown ? [Union, ...([string] | [number])]: never) : never);
29+
// Expected: [string, string] | [number, number] | [string, number] | [number, string]
30+
31+
// Test that the types are actually correct by checking assignability
32+
type Expected = [string, string] | [number, number] | [string, number] | [number, string];
33+
34+
// These should all be true (no error)
35+
type Test1Check = Expected extends typeof test2 ? true : false;
36+
type Test2Check = typeof test2 extends Expected ? true : false;
37+
38+
// If the bug is fixed, these will also be true (no error)
39+
type Test3Check = Expected extends typeof test3 ? true : false;
40+
type Test4Check = typeof test3 extends Expected ? true : false;
41+
42+
type Test5Check = Expected extends typeof test4 ? true : false;
43+
type Test6Check = typeof test4 extends Expected ? true : false;
44+
45+
// Force an error if checks fail
46+
const _check1: Test1Check = true;
47+
const _check2: Test2Check = true;
48+
const _check3: Test3Check = true; // This will error if bug exists
49+
const _check4: Test4Check = true; // This will error if bug exists
50+
const _check5: Test5Check = true;
51+
const _check6: Test6Check = true;
52+
53+
54+
//// [spreadTupleUnionDistribution.js]
55+
"use strict";
56+
// Repro from #62812
57+
// Spread operator fails to distribute over union when recursive type call is inlined instead of aliased
58+
// Basic test - this works
59+
var test1; // [string] | [number]
60+
// With alias - this should work and give full cross product
61+
var test2;
62+
// Expected: [string, string] | [number, number] | [string, number] | [number, string]
63+
// With inlined type - this should also work but currently doesn't distribute properly
64+
var test3;
65+
// Expected: [string, string] | [number, number] | [string, number] | [number, string]
66+
// Actual (bug): [string, string] | [number, number]
67+
// With literal union - this works
68+
var test4;
69+
// Force an error if checks fail
70+
var _check1 = true;
71+
var _check2 = true;
72+
var _check3 = true; // This will error if bug exists
73+
var _check4 = true; // This will error if bug exists
74+
var _check5 = true;
75+
var _check6 = true;
76+
77+
78+
//// [spreadTupleUnionDistribution.d.ts]
79+
type CrossProduct<Union, Counter extends unknown[]> = Counter extends [infer Zero, ...infer Rest] ? (Union extends infer Member ? [Member, ...CrossProduct<Union, Rest>] : never) : [];
80+
declare let test1: CrossProduct<number | string, [undefined]>;
81+
type Depth1 = CrossProduct<number | string, [undefined]>;
82+
declare let test2: (number | string extends infer Union ? (Union extends unknown ? [Union, ...Depth1] : never) : never);
83+
declare let test3: (number | string extends infer Union ? (Union extends unknown ? [Union, ...CrossProduct<number | string, [undefined]>] : never) : never);
84+
declare let test4: (number | string extends infer Union ? (Union extends unknown ? [Union, ...([string] | [number])] : never) : never);
85+
type Expected = [string, string] | [number, number] | [string, number] | [number, string];
86+
type Test1Check = Expected extends typeof test2 ? true : false;
87+
type Test2Check = typeof test2 extends Expected ? true : false;
88+
type Test3Check = Expected extends typeof test3 ? true : false;
89+
type Test4Check = typeof test3 extends Expected ? true : false;
90+
type Test5Check = Expected extends typeof test4 ? true : false;
91+
type Test6Check = typeof test4 extends Expected ? true : false;
92+
declare const _check1: Test1Check;
93+
declare const _check2: Test2Check;
94+
declare const _check3: Test3Check;
95+
declare const _check4: Test4Check;
96+
declare const _check5: Test5Check;
97+
declare const _check6: Test6Check;
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
//// [tests/cases/conformance/types/spread/spreadTupleUnionDistribution.ts] ////
2+
3+
=== spreadTupleUnionDistribution.ts ===
4+
// Repro from #62812
5+
// Spread operator fails to distribute over union when recursive type call is inlined instead of aliased
6+
7+
type CrossProduct<Union, Counter extends unknown[]> =
8+
>CrossProduct : Symbol(CrossProduct, Decl(spreadTupleUnionDistribution.ts, 0, 0))
9+
>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 3, 18))
10+
>Counter : Symbol(Counter, Decl(spreadTupleUnionDistribution.ts, 3, 24))
11+
12+
Counter extends [infer Zero, ...infer Rest]
13+
>Counter : Symbol(Counter, Decl(spreadTupleUnionDistribution.ts, 3, 24))
14+
>Zero : Symbol(Zero, Decl(spreadTupleUnionDistribution.ts, 4, 26))
15+
>Rest : Symbol(Rest, Decl(spreadTupleUnionDistribution.ts, 4, 41))
16+
17+
? (Union extends infer Member
18+
>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 3, 18))
19+
>Member : Symbol(Member, Decl(spreadTupleUnionDistribution.ts, 5, 26))
20+
21+
? [Member, ...CrossProduct<Union, Rest>]
22+
>Member : Symbol(Member, Decl(spreadTupleUnionDistribution.ts, 5, 26))
23+
>CrossProduct : Symbol(CrossProduct, Decl(spreadTupleUnionDistribution.ts, 0, 0))
24+
>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 3, 18))
25+
>Rest : Symbol(Rest, Decl(spreadTupleUnionDistribution.ts, 4, 41))
26+
27+
: never)
28+
: [];
29+
30+
// Basic test - this works
31+
let test1: CrossProduct<number | string, [undefined]>; // [string] | [number]
32+
>test1 : Symbol(test1, Decl(spreadTupleUnionDistribution.ts, 11, 3))
33+
>CrossProduct : Symbol(CrossProduct, Decl(spreadTupleUnionDistribution.ts, 0, 0))
34+
35+
type Depth1 = CrossProduct<number | string, [undefined]> // [string] | [number]
36+
>Depth1 : Symbol(Depth1, Decl(spreadTupleUnionDistribution.ts, 11, 54))
37+
>CrossProduct : Symbol(CrossProduct, Decl(spreadTupleUnionDistribution.ts, 0, 0))
38+
39+
// With alias - this should work and give full cross product
40+
let test2: (number | string extends infer Union ? (Union extends unknown ? [Union, ...Depth1]: never) : never);
41+
>test2 : Symbol(test2, Decl(spreadTupleUnionDistribution.ts, 15, 3))
42+
>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 15, 41))
43+
>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 15, 41))
44+
>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 15, 41))
45+
>Depth1 : Symbol(Depth1, Decl(spreadTupleUnionDistribution.ts, 11, 54))
46+
47+
// Expected: [string, string] | [number, number] | [string, number] | [number, string]
48+
49+
// With inlined type - this should also work but currently doesn't distribute properly
50+
let test3: (number | string extends infer Union ? (Union extends unknown ? [Union, ...CrossProduct<number | string, [undefined]>]: never) : never);
51+
>test3 : Symbol(test3, Decl(spreadTupleUnionDistribution.ts, 19, 3))
52+
>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 19, 41))
53+
>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 19, 41))
54+
>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 19, 41))
55+
>CrossProduct : Symbol(CrossProduct, Decl(spreadTupleUnionDistribution.ts, 0, 0))
56+
57+
// Expected: [string, string] | [number, number] | [string, number] | [number, string]
58+
// Actual (bug): [string, string] | [number, number]
59+
60+
// With literal union - this works
61+
let test4: (number | string extends infer Union ? (Union extends unknown ? [Union, ...([string] | [number])]: never) : never);
62+
>test4 : Symbol(test4, Decl(spreadTupleUnionDistribution.ts, 24, 3))
63+
>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 24, 41))
64+
>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 24, 41))
65+
>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 24, 41))
66+
67+
// Expected: [string, string] | [number, number] | [string, number] | [number, string]
68+
69+
// Test that the types are actually correct by checking assignability
70+
type Expected = [string, string] | [number, number] | [string, number] | [number, string];
71+
>Expected : Symbol(Expected, Decl(spreadTupleUnionDistribution.ts, 24, 126))
72+
73+
// These should all be true (no error)
74+
type Test1Check = Expected extends typeof test2 ? true : false;
75+
>Test1Check : Symbol(Test1Check, Decl(spreadTupleUnionDistribution.ts, 28, 90))
76+
>Expected : Symbol(Expected, Decl(spreadTupleUnionDistribution.ts, 24, 126))
77+
>test2 : Symbol(test2, Decl(spreadTupleUnionDistribution.ts, 15, 3))
78+
79+
type Test2Check = typeof test2 extends Expected ? true : false;
80+
>Test2Check : Symbol(Test2Check, Decl(spreadTupleUnionDistribution.ts, 31, 63))
81+
>test2 : Symbol(test2, Decl(spreadTupleUnionDistribution.ts, 15, 3))
82+
>Expected : Symbol(Expected, Decl(spreadTupleUnionDistribution.ts, 24, 126))
83+
84+
// If the bug is fixed, these will also be true (no error)
85+
type Test3Check = Expected extends typeof test3 ? true : false;
86+
>Test3Check : Symbol(Test3Check, Decl(spreadTupleUnionDistribution.ts, 32, 63))
87+
>Expected : Symbol(Expected, Decl(spreadTupleUnionDistribution.ts, 24, 126))
88+
>test3 : Symbol(test3, Decl(spreadTupleUnionDistribution.ts, 19, 3))
89+
90+
type Test4Check = typeof test3 extends Expected ? true : false;
91+
>Test4Check : Symbol(Test4Check, Decl(spreadTupleUnionDistribution.ts, 35, 63))
92+
>test3 : Symbol(test3, Decl(spreadTupleUnionDistribution.ts, 19, 3))
93+
>Expected : Symbol(Expected, Decl(spreadTupleUnionDistribution.ts, 24, 126))
94+
95+
type Test5Check = Expected extends typeof test4 ? true : false;
96+
>Test5Check : Symbol(Test5Check, Decl(spreadTupleUnionDistribution.ts, 36, 63))
97+
>Expected : Symbol(Expected, Decl(spreadTupleUnionDistribution.ts, 24, 126))
98+
>test4 : Symbol(test4, Decl(spreadTupleUnionDistribution.ts, 24, 3))
99+
100+
type Test6Check = typeof test4 extends Expected ? true : false;
101+
>Test6Check : Symbol(Test6Check, Decl(spreadTupleUnionDistribution.ts, 38, 63))
102+
>test4 : Symbol(test4, Decl(spreadTupleUnionDistribution.ts, 24, 3))
103+
>Expected : Symbol(Expected, Decl(spreadTupleUnionDistribution.ts, 24, 126))
104+
105+
// Force an error if checks fail
106+
const _check1: Test1Check = true;
107+
>_check1 : Symbol(_check1, Decl(spreadTupleUnionDistribution.ts, 42, 5))
108+
>Test1Check : Symbol(Test1Check, Decl(spreadTupleUnionDistribution.ts, 28, 90))
109+
110+
const _check2: Test2Check = true;
111+
>_check2 : Symbol(_check2, Decl(spreadTupleUnionDistribution.ts, 43, 5))
112+
>Test2Check : Symbol(Test2Check, Decl(spreadTupleUnionDistribution.ts, 31, 63))
113+
114+
const _check3: Test3Check = true; // This will error if bug exists
115+
>_check3 : Symbol(_check3, Decl(spreadTupleUnionDistribution.ts, 44, 5))
116+
>Test3Check : Symbol(Test3Check, Decl(spreadTupleUnionDistribution.ts, 32, 63))
117+
118+
const _check4: Test4Check = true; // This will error if bug exists
119+
>_check4 : Symbol(_check4, Decl(spreadTupleUnionDistribution.ts, 45, 5))
120+
>Test4Check : Symbol(Test4Check, Decl(spreadTupleUnionDistribution.ts, 35, 63))
121+
122+
const _check5: Test5Check = true;
123+
>_check5 : Symbol(_check5, Decl(spreadTupleUnionDistribution.ts, 46, 5))
124+
>Test5Check : Symbol(Test5Check, Decl(spreadTupleUnionDistribution.ts, 36, 63))
125+
126+
const _check6: Test6Check = true;
127+
>_check6 : Symbol(_check6, Decl(spreadTupleUnionDistribution.ts, 47, 5))
128+
>Test6Check : Symbol(Test6Check, Decl(spreadTupleUnionDistribution.ts, 38, 63))
129+
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
//// [tests/cases/conformance/types/spread/spreadTupleUnionDistribution.ts] ////
2+
3+
=== spreadTupleUnionDistribution.ts ===
4+
// Repro from #62812
5+
// Spread operator fails to distribute over union when recursive type call is inlined instead of aliased
6+
7+
type CrossProduct<Union, Counter extends unknown[]> =
8+
>CrossProduct : CrossProduct<Union, Counter>
9+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
10+
11+
Counter extends [infer Zero, ...infer Rest]
12+
? (Union extends infer Member
13+
? [Member, ...CrossProduct<Union, Rest>]
14+
: never)
15+
: [];
16+
17+
// Basic test - this works
18+
let test1: CrossProduct<number | string, [undefined]>; // [string] | [number]
19+
>test1 : [string] | [number]
20+
> : ^^^^^^^^^^^^^^^^^^^
21+
22+
type Depth1 = CrossProduct<number | string, [undefined]> // [string] | [number]
23+
>Depth1 : [string] | [number]
24+
> : ^^^^^^^^^^^^^^^^^^^
25+
26+
// With alias - this should work and give full cross product
27+
let test2: (number | string extends infer Union ? (Union extends unknown ? [Union, ...Depth1]: never) : never);
28+
>test2 : [string, string] | [string, number] | [number, string] | [number, number]
29+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
30+
31+
// Expected: [string, string] | [number, number] | [string, number] | [number, string]
32+
33+
// With inlined type - this should also work but currently doesn't distribute properly
34+
let test3: (number | string extends infer Union ? (Union extends unknown ? [Union, ...CrossProduct<number | string, [undefined]>]: never) : never);
35+
>test3 : [string, string] | [string, number] | [number, string] | [number, number]
36+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
37+
38+
// Expected: [string, string] | [number, number] | [string, number] | [number, string]
39+
// Actual (bug): [string, string] | [number, number]
40+
41+
// With literal union - this works
42+
let test4: (number | string extends infer Union ? (Union extends unknown ? [Union, ...([string] | [number])]: never) : never);
43+
>test4 : [string, string] | [string, number] | [number, string] | [number, number]
44+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
45+
46+
// Expected: [string, string] | [number, number] | [string, number] | [number, string]
47+
48+
// Test that the types are actually correct by checking assignability
49+
type Expected = [string, string] | [number, number] | [string, number] | [number, string];
50+
>Expected : Expected
51+
> : ^^^^^^^^
52+
53+
// These should all be true (no error)
54+
type Test1Check = Expected extends typeof test2 ? true : false;
55+
>Test1Check : true
56+
> : ^^^^
57+
>test2 : [string, string] | [string, number] | [number, string] | [number, number]
58+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
59+
>true : true
60+
> : ^^^^
61+
>false : false
62+
> : ^^^^^
63+
64+
type Test2Check = typeof test2 extends Expected ? true : false;
65+
>Test2Check : true
66+
> : ^^^^
67+
>test2 : [string, string] | [string, number] | [number, string] | [number, number]
68+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
69+
>true : true
70+
> : ^^^^
71+
>false : false
72+
> : ^^^^^
73+
74+
// If the bug is fixed, these will also be true (no error)
75+
type Test3Check = Expected extends typeof test3 ? true : false;
76+
>Test3Check : true
77+
> : ^^^^
78+
>test3 : [string, string] | [string, number] | [number, string] | [number, number]
79+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
80+
>true : true
81+
> : ^^^^
82+
>false : false
83+
> : ^^^^^
84+
85+
type Test4Check = typeof test3 extends Expected ? true : false;
86+
>Test4Check : true
87+
> : ^^^^
88+
>test3 : [string, string] | [string, number] | [number, string] | [number, number]
89+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
90+
>true : true
91+
> : ^^^^
92+
>false : false
93+
> : ^^^^^
94+
95+
type Test5Check = Expected extends typeof test4 ? true : false;
96+
>Test5Check : true
97+
> : ^^^^
98+
>test4 : [string, string] | [string, number] | [number, string] | [number, number]
99+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
100+
>true : true
101+
> : ^^^^
102+
>false : false
103+
> : ^^^^^
104+
105+
type Test6Check = typeof test4 extends Expected ? true : false;
106+
>Test6Check : true
107+
> : ^^^^
108+
>test4 : [string, string] | [string, number] | [number, string] | [number, number]
109+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
110+
>true : true
111+
> : ^^^^
112+
>false : false
113+
> : ^^^^^
114+
115+
// Force an error if checks fail
116+
const _check1: Test1Check = true;
117+
>_check1 : true
118+
> : ^^^^
119+
>true : true
120+
> : ^^^^
121+
122+
const _check2: Test2Check = true;
123+
>_check2 : true
124+
> : ^^^^
125+
>true : true
126+
> : ^^^^
127+
128+
const _check3: Test3Check = true; // This will error if bug exists
129+
>_check3 : true
130+
> : ^^^^
131+
>true : true
132+
> : ^^^^
133+
134+
const _check4: Test4Check = true; // This will error if bug exists
135+
>_check4 : true
136+
> : ^^^^
137+
>true : true
138+
> : ^^^^
139+
140+
const _check5: Test5Check = true;
141+
>_check5 : true
142+
> : ^^^^
143+
>true : true
144+
> : ^^^^
145+
146+
const _check6: Test6Check = true;
147+
>_check6 : true
148+
> : ^^^^
149+
>true : true
150+
> : ^^^^
151+

0 commit comments

Comments
 (0)