diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index bfa81f6964601..227a862efd73a 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -17200,8 +17200,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { covariant = !covariant; } // Always substitute on type parameters, regardless of variance, since even - // in contravariant positions, they may rely on substituted constraints to be valid - if ((covariant || type.flags & TypeFlags.TypeVariable) && parent.kind === SyntaxKind.ConditionalType && node === (parent as ConditionalTypeNode).trueType) { + // in contravariant positions, they may rely on substituted constraints to be valid. + // For union/intersection types that don't contain type variables, don't apply narrowing + // based on structural equality with the check type - different occurrences of + // `number | string` in the code are independent even if they resolve to the same + // canonical type. Named types (interfaces, etc.) should still be narrowed since + // different references to the same named type refer to the same entity. + const isStructuralTypeWithoutTypeVariables = !!(type.flags & TypeFlags.UnionOrIntersection) && !couldContainTypeVariables(type); + if (!isStructuralTypeWithoutTypeVariables && (covariant || type.flags & TypeFlags.TypeVariable) && parent.kind === SyntaxKind.ConditionalType && node === (parent as ConditionalTypeNode).trueType) { const constraint = getImpliedConstraint(type, (parent as ConditionalTypeNode).checkType, (parent as ConditionalTypeNode).extendsType); if (constraint) { constraints = append(constraints, constraint); diff --git a/tests/baselines/reference/spreadTupleUnionDistribution.js b/tests/baselines/reference/spreadTupleUnionDistribution.js new file mode 100644 index 0000000000000..89c8ea8696531 --- /dev/null +++ b/tests/baselines/reference/spreadTupleUnionDistribution.js @@ -0,0 +1,97 @@ +//// [tests/cases/conformance/types/spread/spreadTupleUnionDistribution.ts] //// + +//// [spreadTupleUnionDistribution.ts] +// Repro from #62812 +// Spread operator fails to distribute over union when recursive type call is inlined instead of aliased + +type CrossProduct = + Counter extends [infer Zero, ...infer Rest] + ? (Union extends infer Member + ? [Member, ...CrossProduct] + : never) + : []; + +// Basic test - this works +let test1: CrossProduct; // [string] | [number] +type Depth1 = CrossProduct // [string] | [number] + +// With alias - this should work and give full cross product +let test2: (number | string extends infer Union ? (Union extends unknown ? [Union, ...Depth1]: never) : never); +// Expected: [string, string] | [number, number] | [string, number] | [number, string] + +// With inlined type - this should also work but currently doesn't distribute properly +let test3: (number | string extends infer Union ? (Union extends unknown ? [Union, ...CrossProduct]: never) : never); +// Expected: [string, string] | [number, number] | [string, number] | [number, string] +// Actual (bug): [string, string] | [number, number] + +// With literal union - this works +let test4: (number | string extends infer Union ? (Union extends unknown ? [Union, ...([string] | [number])]: never) : never); +// Expected: [string, string] | [number, number] | [string, number] | [number, string] + +// Test that the types are actually correct by checking assignability +type Expected = [string, string] | [number, number] | [string, number] | [number, string]; + +// These should all be true (no error) +type Test1Check = Expected extends typeof test2 ? true : false; +type Test2Check = typeof test2 extends Expected ? true : false; + +// If the bug is fixed, these will also be true (no error) +type Test3Check = Expected extends typeof test3 ? true : false; +type Test4Check = typeof test3 extends Expected ? true : false; + +type Test5Check = Expected extends typeof test4 ? true : false; +type Test6Check = typeof test4 extends Expected ? true : false; + +// Force an error if checks fail +const _check1: Test1Check = true; +const _check2: Test2Check = true; +const _check3: Test3Check = true; // This will error if bug exists +const _check4: Test4Check = true; // This will error if bug exists +const _check5: Test5Check = true; +const _check6: Test6Check = true; + + +//// [spreadTupleUnionDistribution.js] +"use strict"; +// Repro from #62812 +// Spread operator fails to distribute over union when recursive type call is inlined instead of aliased +// Basic test - this works +var test1; // [string] | [number] +// With alias - this should work and give full cross product +var test2; +// Expected: [string, string] | [number, number] | [string, number] | [number, string] +// With inlined type - this should also work but currently doesn't distribute properly +var test3; +// Expected: [string, string] | [number, number] | [string, number] | [number, string] +// Actual (bug): [string, string] | [number, number] +// With literal union - this works +var test4; +// Force an error if checks fail +var _check1 = true; +var _check2 = true; +var _check3 = true; // This will error if bug exists +var _check4 = true; // This will error if bug exists +var _check5 = true; +var _check6 = true; + + +//// [spreadTupleUnionDistribution.d.ts] +type CrossProduct = Counter extends [infer Zero, ...infer Rest] ? (Union extends infer Member ? [Member, ...CrossProduct] : never) : []; +declare let test1: CrossProduct; +type Depth1 = CrossProduct; +declare let test2: (number | string extends infer Union ? (Union extends unknown ? [Union, ...Depth1] : never) : never); +declare let test3: (number | string extends infer Union ? (Union extends unknown ? [Union, ...CrossProduct] : never) : never); +declare let test4: (number | string extends infer Union ? (Union extends unknown ? [Union, ...([string] | [number])] : never) : never); +type Expected = [string, string] | [number, number] | [string, number] | [number, string]; +type Test1Check = Expected extends typeof test2 ? true : false; +type Test2Check = typeof test2 extends Expected ? true : false; +type Test3Check = Expected extends typeof test3 ? true : false; +type Test4Check = typeof test3 extends Expected ? true : false; +type Test5Check = Expected extends typeof test4 ? true : false; +type Test6Check = typeof test4 extends Expected ? true : false; +declare const _check1: Test1Check; +declare const _check2: Test2Check; +declare const _check3: Test3Check; +declare const _check4: Test4Check; +declare const _check5: Test5Check; +declare const _check6: Test6Check; diff --git a/tests/baselines/reference/spreadTupleUnionDistribution.symbols b/tests/baselines/reference/spreadTupleUnionDistribution.symbols new file mode 100644 index 0000000000000..6639d34abd7ae --- /dev/null +++ b/tests/baselines/reference/spreadTupleUnionDistribution.symbols @@ -0,0 +1,129 @@ +//// [tests/cases/conformance/types/spread/spreadTupleUnionDistribution.ts] //// + +=== spreadTupleUnionDistribution.ts === +// Repro from #62812 +// Spread operator fails to distribute over union when recursive type call is inlined instead of aliased + +type CrossProduct = +>CrossProduct : Symbol(CrossProduct, Decl(spreadTupleUnionDistribution.ts, 0, 0)) +>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 3, 18)) +>Counter : Symbol(Counter, Decl(spreadTupleUnionDistribution.ts, 3, 24)) + + Counter extends [infer Zero, ...infer Rest] +>Counter : Symbol(Counter, Decl(spreadTupleUnionDistribution.ts, 3, 24)) +>Zero : Symbol(Zero, Decl(spreadTupleUnionDistribution.ts, 4, 26)) +>Rest : Symbol(Rest, Decl(spreadTupleUnionDistribution.ts, 4, 41)) + + ? (Union extends infer Member +>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 3, 18)) +>Member : Symbol(Member, Decl(spreadTupleUnionDistribution.ts, 5, 26)) + + ? [Member, ...CrossProduct] +>Member : Symbol(Member, Decl(spreadTupleUnionDistribution.ts, 5, 26)) +>CrossProduct : Symbol(CrossProduct, Decl(spreadTupleUnionDistribution.ts, 0, 0)) +>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 3, 18)) +>Rest : Symbol(Rest, Decl(spreadTupleUnionDistribution.ts, 4, 41)) + + : never) + : []; + +// Basic test - this works +let test1: CrossProduct; // [string] | [number] +>test1 : Symbol(test1, Decl(spreadTupleUnionDistribution.ts, 11, 3)) +>CrossProduct : Symbol(CrossProduct, Decl(spreadTupleUnionDistribution.ts, 0, 0)) + +type Depth1 = CrossProduct // [string] | [number] +>Depth1 : Symbol(Depth1, Decl(spreadTupleUnionDistribution.ts, 11, 54)) +>CrossProduct : Symbol(CrossProduct, Decl(spreadTupleUnionDistribution.ts, 0, 0)) + +// With alias - this should work and give full cross product +let test2: (number | string extends infer Union ? (Union extends unknown ? [Union, ...Depth1]: never) : never); +>test2 : Symbol(test2, Decl(spreadTupleUnionDistribution.ts, 15, 3)) +>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 15, 41)) +>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 15, 41)) +>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 15, 41)) +>Depth1 : Symbol(Depth1, Decl(spreadTupleUnionDistribution.ts, 11, 54)) + +// Expected: [string, string] | [number, number] | [string, number] | [number, string] + +// With inlined type - this should also work but currently doesn't distribute properly +let test3: (number | string extends infer Union ? (Union extends unknown ? [Union, ...CrossProduct]: never) : never); +>test3 : Symbol(test3, Decl(spreadTupleUnionDistribution.ts, 19, 3)) +>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 19, 41)) +>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 19, 41)) +>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 19, 41)) +>CrossProduct : Symbol(CrossProduct, Decl(spreadTupleUnionDistribution.ts, 0, 0)) + +// Expected: [string, string] | [number, number] | [string, number] | [number, string] +// Actual (bug): [string, string] | [number, number] + +// With literal union - this works +let test4: (number | string extends infer Union ? (Union extends unknown ? [Union, ...([string] | [number])]: never) : never); +>test4 : Symbol(test4, Decl(spreadTupleUnionDistribution.ts, 24, 3)) +>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 24, 41)) +>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 24, 41)) +>Union : Symbol(Union, Decl(spreadTupleUnionDistribution.ts, 24, 41)) + +// Expected: [string, string] | [number, number] | [string, number] | [number, string] + +// Test that the types are actually correct by checking assignability +type Expected = [string, string] | [number, number] | [string, number] | [number, string]; +>Expected : Symbol(Expected, Decl(spreadTupleUnionDistribution.ts, 24, 126)) + +// These should all be true (no error) +type Test1Check = Expected extends typeof test2 ? true : false; +>Test1Check : Symbol(Test1Check, Decl(spreadTupleUnionDistribution.ts, 28, 90)) +>Expected : Symbol(Expected, Decl(spreadTupleUnionDistribution.ts, 24, 126)) +>test2 : Symbol(test2, Decl(spreadTupleUnionDistribution.ts, 15, 3)) + +type Test2Check = typeof test2 extends Expected ? true : false; +>Test2Check : Symbol(Test2Check, Decl(spreadTupleUnionDistribution.ts, 31, 63)) +>test2 : Symbol(test2, Decl(spreadTupleUnionDistribution.ts, 15, 3)) +>Expected : Symbol(Expected, Decl(spreadTupleUnionDistribution.ts, 24, 126)) + +// If the bug is fixed, these will also be true (no error) +type Test3Check = Expected extends typeof test3 ? true : false; +>Test3Check : Symbol(Test3Check, Decl(spreadTupleUnionDistribution.ts, 32, 63)) +>Expected : Symbol(Expected, Decl(spreadTupleUnionDistribution.ts, 24, 126)) +>test3 : Symbol(test3, Decl(spreadTupleUnionDistribution.ts, 19, 3)) + +type Test4Check = typeof test3 extends Expected ? true : false; +>Test4Check : Symbol(Test4Check, Decl(spreadTupleUnionDistribution.ts, 35, 63)) +>test3 : Symbol(test3, Decl(spreadTupleUnionDistribution.ts, 19, 3)) +>Expected : Symbol(Expected, Decl(spreadTupleUnionDistribution.ts, 24, 126)) + +type Test5Check = Expected extends typeof test4 ? true : false; +>Test5Check : Symbol(Test5Check, Decl(spreadTupleUnionDistribution.ts, 36, 63)) +>Expected : Symbol(Expected, Decl(spreadTupleUnionDistribution.ts, 24, 126)) +>test4 : Symbol(test4, Decl(spreadTupleUnionDistribution.ts, 24, 3)) + +type Test6Check = typeof test4 extends Expected ? true : false; +>Test6Check : Symbol(Test6Check, Decl(spreadTupleUnionDistribution.ts, 38, 63)) +>test4 : Symbol(test4, Decl(spreadTupleUnionDistribution.ts, 24, 3)) +>Expected : Symbol(Expected, Decl(spreadTupleUnionDistribution.ts, 24, 126)) + +// Force an error if checks fail +const _check1: Test1Check = true; +>_check1 : Symbol(_check1, Decl(spreadTupleUnionDistribution.ts, 42, 5)) +>Test1Check : Symbol(Test1Check, Decl(spreadTupleUnionDistribution.ts, 28, 90)) + +const _check2: Test2Check = true; +>_check2 : Symbol(_check2, Decl(spreadTupleUnionDistribution.ts, 43, 5)) +>Test2Check : Symbol(Test2Check, Decl(spreadTupleUnionDistribution.ts, 31, 63)) + +const _check3: Test3Check = true; // This will error if bug exists +>_check3 : Symbol(_check3, Decl(spreadTupleUnionDistribution.ts, 44, 5)) +>Test3Check : Symbol(Test3Check, Decl(spreadTupleUnionDistribution.ts, 32, 63)) + +const _check4: Test4Check = true; // This will error if bug exists +>_check4 : Symbol(_check4, Decl(spreadTupleUnionDistribution.ts, 45, 5)) +>Test4Check : Symbol(Test4Check, Decl(spreadTupleUnionDistribution.ts, 35, 63)) + +const _check5: Test5Check = true; +>_check5 : Symbol(_check5, Decl(spreadTupleUnionDistribution.ts, 46, 5)) +>Test5Check : Symbol(Test5Check, Decl(spreadTupleUnionDistribution.ts, 36, 63)) + +const _check6: Test6Check = true; +>_check6 : Symbol(_check6, Decl(spreadTupleUnionDistribution.ts, 47, 5)) +>Test6Check : Symbol(Test6Check, Decl(spreadTupleUnionDistribution.ts, 38, 63)) + diff --git a/tests/baselines/reference/spreadTupleUnionDistribution.types b/tests/baselines/reference/spreadTupleUnionDistribution.types new file mode 100644 index 0000000000000..33b3cf153aba6 --- /dev/null +++ b/tests/baselines/reference/spreadTupleUnionDistribution.types @@ -0,0 +1,151 @@ +//// [tests/cases/conformance/types/spread/spreadTupleUnionDistribution.ts] //// + +=== spreadTupleUnionDistribution.ts === +// Repro from #62812 +// Spread operator fails to distribute over union when recursive type call is inlined instead of aliased + +type CrossProduct = +>CrossProduct : CrossProduct +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Counter extends [infer Zero, ...infer Rest] + ? (Union extends infer Member + ? [Member, ...CrossProduct] + : never) + : []; + +// Basic test - this works +let test1: CrossProduct; // [string] | [number] +>test1 : [string] | [number] +> : ^^^^^^^^^^^^^^^^^^^ + +type Depth1 = CrossProduct // [string] | [number] +>Depth1 : [string] | [number] +> : ^^^^^^^^^^^^^^^^^^^ + +// With alias - this should work and give full cross product +let test2: (number | string extends infer Union ? (Union extends unknown ? [Union, ...Depth1]: never) : never); +>test2 : [string, string] | [string, number] | [number, string] | [number, number] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +// Expected: [string, string] | [number, number] | [string, number] | [number, string] + +// With inlined type - this should also work but currently doesn't distribute properly +let test3: (number | string extends infer Union ? (Union extends unknown ? [Union, ...CrossProduct]: never) : never); +>test3 : [string, string] | [string, number] | [number, string] | [number, number] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +// Expected: [string, string] | [number, number] | [string, number] | [number, string] +// Actual (bug): [string, string] | [number, number] + +// With literal union - this works +let test4: (number | string extends infer Union ? (Union extends unknown ? [Union, ...([string] | [number])]: never) : never); +>test4 : [string, string] | [string, number] | [number, string] | [number, number] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +// Expected: [string, string] | [number, number] | [string, number] | [number, string] + +// Test that the types are actually correct by checking assignability +type Expected = [string, string] | [number, number] | [string, number] | [number, string]; +>Expected : Expected +> : ^^^^^^^^ + +// These should all be true (no error) +type Test1Check = Expected extends typeof test2 ? true : false; +>Test1Check : true +> : ^^^^ +>test2 : [string, string] | [string, number] | [number, string] | [number, number] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>true : true +> : ^^^^ +>false : false +> : ^^^^^ + +type Test2Check = typeof test2 extends Expected ? true : false; +>Test2Check : true +> : ^^^^ +>test2 : [string, string] | [string, number] | [number, string] | [number, number] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>true : true +> : ^^^^ +>false : false +> : ^^^^^ + +// If the bug is fixed, these will also be true (no error) +type Test3Check = Expected extends typeof test3 ? true : false; +>Test3Check : true +> : ^^^^ +>test3 : [string, string] | [string, number] | [number, string] | [number, number] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>true : true +> : ^^^^ +>false : false +> : ^^^^^ + +type Test4Check = typeof test3 extends Expected ? true : false; +>Test4Check : true +> : ^^^^ +>test3 : [string, string] | [string, number] | [number, string] | [number, number] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>true : true +> : ^^^^ +>false : false +> : ^^^^^ + +type Test5Check = Expected extends typeof test4 ? true : false; +>Test5Check : true +> : ^^^^ +>test4 : [string, string] | [string, number] | [number, string] | [number, number] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>true : true +> : ^^^^ +>false : false +> : ^^^^^ + +type Test6Check = typeof test4 extends Expected ? true : false; +>Test6Check : true +> : ^^^^ +>test4 : [string, string] | [string, number] | [number, string] | [number, number] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>true : true +> : ^^^^ +>false : false +> : ^^^^^ + +// Force an error if checks fail +const _check1: Test1Check = true; +>_check1 : true +> : ^^^^ +>true : true +> : ^^^^ + +const _check2: Test2Check = true; +>_check2 : true +> : ^^^^ +>true : true +> : ^^^^ + +const _check3: Test3Check = true; // This will error if bug exists +>_check3 : true +> : ^^^^ +>true : true +> : ^^^^ + +const _check4: Test4Check = true; // This will error if bug exists +>_check4 : true +> : ^^^^ +>true : true +> : ^^^^ + +const _check5: Test5Check = true; +>_check5 : true +> : ^^^^ +>true : true +> : ^^^^ + +const _check6: Test6Check = true; +>_check6 : true +> : ^^^^ +>true : true +> : ^^^^ + diff --git a/tests/cases/conformance/types/spread/spreadTupleUnionDistribution.ts b/tests/cases/conformance/types/spread/spreadTupleUnionDistribution.ts new file mode 100644 index 0000000000000..466ab5c9a9a79 --- /dev/null +++ b/tests/cases/conformance/types/spread/spreadTupleUnionDistribution.ts @@ -0,0 +1,51 @@ +// @strict: true +// @declaration: true + +// Repro from #62812 +// Spread operator fails to distribute over union when recursive type call is inlined instead of aliased + +type CrossProduct = + Counter extends [infer Zero, ...infer Rest] + ? (Union extends infer Member + ? [Member, ...CrossProduct] + : never) + : []; + +// Basic test - this works +let test1: CrossProduct; // [string] | [number] +type Depth1 = CrossProduct // [string] | [number] + +// With alias - this should work and give full cross product +let test2: (number | string extends infer Union ? (Union extends unknown ? [Union, ...Depth1]: never) : never); +// Expected: [string, string] | [number, number] | [string, number] | [number, string] + +// With inlined type - this should also work but currently doesn't distribute properly +let test3: (number | string extends infer Union ? (Union extends unknown ? [Union, ...CrossProduct]: never) : never); +// Expected: [string, string] | [number, number] | [string, number] | [number, string] +// Actual (bug): [string, string] | [number, number] + +// With literal union - this works +let test4: (number | string extends infer Union ? (Union extends unknown ? [Union, ...([string] | [number])]: never) : never); +// Expected: [string, string] | [number, number] | [string, number] | [number, string] + +// Test that the types are actually correct by checking assignability +type Expected = [string, string] | [number, number] | [string, number] | [number, string]; + +// These should all be true (no error) +type Test1Check = Expected extends typeof test2 ? true : false; +type Test2Check = typeof test2 extends Expected ? true : false; + +// If the bug is fixed, these will also be true (no error) +type Test3Check = Expected extends typeof test3 ? true : false; +type Test4Check = typeof test3 extends Expected ? true : false; + +type Test5Check = Expected extends typeof test4 ? true : false; +type Test6Check = typeof test4 extends Expected ? true : false; + +// Force an error if checks fail +const _check1: Test1Check = true; +const _check2: Test2Check = true; +const _check3: Test3Check = true; // This will error if bug exists +const _check4: Test4Check = true; // This will error if bug exists +const _check5: Test5Check = true; +const _check6: Test6Check = true;