Fix spread operator failing to distribute over union when type is inlined
Fixes #62812
Problem
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.
For example:
type CrossProduct<Union, Counter extends unknown[]> =
Counter extends [infer Zero, ...infer Rest]
? (Union extends infer Member
? [Member, ...CrossProduct<Union, Rest>]
: never)
: [];
type Depth1 = CrossProduct<number | string, [undefined]> // [string] | [number]
// This works correctly:
let test2: (number | string extends infer Union ? (Union extends unknown ? [Union, ...Depth1]: never) : never);
// Result: [string, string] | [number, number] | [string, number] | [number, string]
// But this was broken (inlined instead of aliased):
let test3: (number | string extends infer Union ? (Union extends unknown ? [Union, ...CrossProduct<number | string, [undefined]>]: never) : never);
// Expected: [string, string] | [number, number] | [string, number] | [number, string]
// Actual: [string, string] | [number, number] (missing cross-product entries)
Root Cause
In getConditionalFlowTypeOfType, when processing a type node in the true branch of a conditional type, the function checks if the type matches the conditional's check type and creates a substitution type with the implied constraint.
The issue is that for structural types like number | string, different occurrences in the code all resolve to the same canonical type. So when CrossProduct<number | string, [undefined]> appeared inside the true branch of number | string extends infer Union ? ..., the type argument number | string was incorrectly being narrowed with the constraint Union even though it was a completely independent occurrence.
Fix
Added a check to skip this narrowing for union/intersection types that don't contain type variables:
const isStructuralTypeWithoutTypeVariables = !!(type.flags & TypeFlags.UnionOrIntersection) && !couldContainTypeVariables(type);
if (!isStructuralTypeWithoutTypeVariables && (covariant || type.flags & TypeFlags.TypeVariable) && ...) {
This ensures:
- Named types (interfaces, classes, type aliases) still get narrowed - different references to the same named type refer to the same entity
- Structural types (unions, intersections) without type variables are NOT narrowed - different occurrences are independent even if they structurally match
Test
Added tests/cases/conformance/types/spread/spreadTupleUnionDistribution.ts to verify the fix.
@microsoft-github-policy-service agree
@RyanCavanaugh