Skip to content

Fix contextual typing sensitivity to binding pattern structure in destructuring #62142

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
45 changes: 45 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22901,6 +22901,47 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
) {
return false;
}

// Heuristic: If the target type looks like a constraint (simple object type with few properties),
// be more lenient with excess property checking. This handles cases like T extends { prop: Type }
// where the constraint should allow additional properties.
if (target.flags & TypeFlags.Object && !isComparingJsxAttributes) {
const targetProperties = getPropertiesOfType(target);
const targetIndexInfos = getIndexInfosOfType(target);
// If it's a simple object with few properties and no index signatures, it might be a constraint
if (targetProperties.length <= 2 && targetIndexInfos.length === 0) {
// Additional check: at least one property should be a simple string literal union (common in constraints)
// This helps distinguish constraints like { dataType: 'a' | 'b' } from complex intersection types
let hasSimpleUnionProperty = false;
for (const targetProp of targetProperties) {
const propType = getTypeOfSymbol(targetProp);
if (propType.flags & TypeFlags.Union) {
const unionType = propType as UnionType;
// Check if it's a union of string literals (typical of enum-like constraints)
if (unionType.types.length <= 5 && unionType.types.every(t => t.flags & TypeFlags.StringLiteral)) {
hasSimpleUnionProperty = true;
break;
}
}
}

if (hasSimpleUnionProperty) {
// Check if all properties in the target exist in the source
let allTargetPropsExist = true;
for (const targetProp of targetProperties) {
if (!source.symbol?.members?.has(targetProp.escapedName)) {
allTargetPropsExist = false;
break;
}
}
// If the source contains all target properties, likely this is a constraint scenario
if (allTargetPropsExist) {
return false;
}
}
}
}

let reducedTarget = target;
let checkTypes: Type[] | undefined;
if (target.flags & TypeFlags.Union) {
Expand Down Expand Up @@ -34281,6 +34322,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (targetType.flags & TypeFlags.Substitution) {
return isKnownProperty((targetType as SubstitutionType).baseType, name, isComparingJsxAttributes);
}
if (targetType.flags & TypeFlags.TypeParameter) {
const constraint = getConstraintOfTypeParameter(targetType as TypeParameter);
return constraint ? isKnownProperty(constraint, name, isComparingJsxAttributes) : false;
}
if (targetType.flags & TypeFlags.UnionOrIntersection && isExcessPropertyCheckTarget(targetType)) {
for (const t of (targetType as UnionOrIntersectionType).types) {
if (isKnownProperty(t, name, isComparingJsxAttributes)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//// [tests/cases/compiler/destructuringAssignmentWithConstraints.ts] ////

//// [destructuringAssignmentWithConstraints.ts]
// Test case for destructuring assignment with generic constraints issue

type DataType = 'a' | 'b';

declare function foo<T extends { dataType: DataType }>(template: T): [T, any, any];
declare function bar<T extends { dataType: DataType }>(template: T): [T, any];

function testDestructuringBug() {
// These work fine (and should continue to work)
const [, ,] = foo({ dataType: 'a', day: 0 });
const [x, y, z] = foo({ dataType: 'a', day: 0 });
const [,] = bar({ dataType: 'a', day: 0 });
const [a, b] = bar({ dataType: 'a', day: 0 });

// These should work but currently don't (this is the bug)
const [, , t] = foo({ dataType: 'a', day: 0 }); // Should not error
const [, u] = bar({ dataType: 'a', day: 0 }); // Should not error

console.log(x, y, z, t, a, b, u);
}

// Test that direct calls work fine (they do)
function testDirectCalls() {
const result1 = foo({ dataType: 'a', day: 0 });
const result2 = bar({ dataType: 'a', day: 0 });
console.log(result1, result2);
}

//// [destructuringAssignmentWithConstraints.js]
// Test case for destructuring assignment with generic constraints issue
function testDestructuringBug() {
// These work fine (and should continue to work)
var _a = foo({ dataType: 'a', day: 0 });
var _b = foo({ dataType: 'a', day: 0 }), x = _b[0], y = _b[1], z = _b[2];
var _c = bar({ dataType: 'a', day: 0 });
var _d = bar({ dataType: 'a', day: 0 }), a = _d[0], b = _d[1];
// These should work but currently don't (this is the bug)
var _e = foo({ dataType: 'a', day: 0 }), t = _e[2]; // Should not error
var _f = bar({ dataType: 'a', day: 0 }), u = _f[1]; // Should not error
console.log(x, y, z, t, a, b, u);
}
// Test that direct calls work fine (they do)
function testDirectCalls() {
var result1 = foo({ dataType: 'a', day: 0 });
var result2 = bar({ dataType: 'a', day: 0 });
console.log(result1, result2);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//// [tests/cases/compiler/destructuringAssignmentWithConstraints.ts] ////

=== destructuringAssignmentWithConstraints.ts ===
// Test case for destructuring assignment with generic constraints issue

type DataType = 'a' | 'b';
>DataType : Symbol(DataType, Decl(destructuringAssignmentWithConstraints.ts, 0, 0))

declare function foo<T extends { dataType: DataType }>(template: T): [T, any, any];
>foo : Symbol(foo, Decl(destructuringAssignmentWithConstraints.ts, 2, 26))
>T : Symbol(T, Decl(destructuringAssignmentWithConstraints.ts, 4, 21))
>dataType : Symbol(dataType, Decl(destructuringAssignmentWithConstraints.ts, 4, 32))
>DataType : Symbol(DataType, Decl(destructuringAssignmentWithConstraints.ts, 0, 0))
>template : Symbol(template, Decl(destructuringAssignmentWithConstraints.ts, 4, 55))
>T : Symbol(T, Decl(destructuringAssignmentWithConstraints.ts, 4, 21))
>T : Symbol(T, Decl(destructuringAssignmentWithConstraints.ts, 4, 21))

declare function bar<T extends { dataType: DataType }>(template: T): [T, any];
>bar : Symbol(bar, Decl(destructuringAssignmentWithConstraints.ts, 4, 83))
>T : Symbol(T, Decl(destructuringAssignmentWithConstraints.ts, 5, 21))
>dataType : Symbol(dataType, Decl(destructuringAssignmentWithConstraints.ts, 5, 32))
>DataType : Symbol(DataType, Decl(destructuringAssignmentWithConstraints.ts, 0, 0))
>template : Symbol(template, Decl(destructuringAssignmentWithConstraints.ts, 5, 55))
>T : Symbol(T, Decl(destructuringAssignmentWithConstraints.ts, 5, 21))
>T : Symbol(T, Decl(destructuringAssignmentWithConstraints.ts, 5, 21))

function testDestructuringBug() {
>testDestructuringBug : Symbol(testDestructuringBug, Decl(destructuringAssignmentWithConstraints.ts, 5, 78))

// These work fine (and should continue to work)
const [, ,] = foo({ dataType: 'a', day: 0 });
>foo : Symbol(foo, Decl(destructuringAssignmentWithConstraints.ts, 2, 26))
>dataType : Symbol(dataType, Decl(destructuringAssignmentWithConstraints.ts, 9, 21))
>day : Symbol(day, Decl(destructuringAssignmentWithConstraints.ts, 9, 36))

const [x, y, z] = foo({ dataType: 'a', day: 0 });
>x : Symbol(x, Decl(destructuringAssignmentWithConstraints.ts, 10, 9))
>y : Symbol(y, Decl(destructuringAssignmentWithConstraints.ts, 10, 11))
>z : Symbol(z, Decl(destructuringAssignmentWithConstraints.ts, 10, 14))
>foo : Symbol(foo, Decl(destructuringAssignmentWithConstraints.ts, 2, 26))
>dataType : Symbol(dataType, Decl(destructuringAssignmentWithConstraints.ts, 10, 25))
>day : Symbol(day, Decl(destructuringAssignmentWithConstraints.ts, 10, 40))

const [,] = bar({ dataType: 'a', day: 0 });
>bar : Symbol(bar, Decl(destructuringAssignmentWithConstraints.ts, 4, 83))
>dataType : Symbol(dataType, Decl(destructuringAssignmentWithConstraints.ts, 11, 19))
>day : Symbol(day, Decl(destructuringAssignmentWithConstraints.ts, 11, 34))

const [a, b] = bar({ dataType: 'a', day: 0 });
>a : Symbol(a, Decl(destructuringAssignmentWithConstraints.ts, 12, 9))
>b : Symbol(b, Decl(destructuringAssignmentWithConstraints.ts, 12, 11))
>bar : Symbol(bar, Decl(destructuringAssignmentWithConstraints.ts, 4, 83))
>dataType : Symbol(dataType, Decl(destructuringAssignmentWithConstraints.ts, 12, 22))
>day : Symbol(day, Decl(destructuringAssignmentWithConstraints.ts, 12, 37))

// These should work but currently don't (this is the bug)
const [, , t] = foo({ dataType: 'a', day: 0 }); // Should not error
>t : Symbol(t, Decl(destructuringAssignmentWithConstraints.ts, 15, 12))
>foo : Symbol(foo, Decl(destructuringAssignmentWithConstraints.ts, 2, 26))
>dataType : Symbol(dataType, Decl(destructuringAssignmentWithConstraints.ts, 15, 23))
>day : Symbol(day, Decl(destructuringAssignmentWithConstraints.ts, 15, 38))

const [, u] = bar({ dataType: 'a', day: 0 }); // Should not error
>u : Symbol(u, Decl(destructuringAssignmentWithConstraints.ts, 16, 10))
>bar : Symbol(bar, Decl(destructuringAssignmentWithConstraints.ts, 4, 83))
>dataType : Symbol(dataType, Decl(destructuringAssignmentWithConstraints.ts, 16, 21))
>day : Symbol(day, Decl(destructuringAssignmentWithConstraints.ts, 16, 36))

console.log(x, y, z, t, a, b, u);
>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
>console : Symbol(console, Decl(lib.dom.d.ts, --, --))
>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
>x : Symbol(x, Decl(destructuringAssignmentWithConstraints.ts, 10, 9))
>y : Symbol(y, Decl(destructuringAssignmentWithConstraints.ts, 10, 11))
>z : Symbol(z, Decl(destructuringAssignmentWithConstraints.ts, 10, 14))
>t : Symbol(t, Decl(destructuringAssignmentWithConstraints.ts, 15, 12))
>a : Symbol(a, Decl(destructuringAssignmentWithConstraints.ts, 12, 9))
>b : Symbol(b, Decl(destructuringAssignmentWithConstraints.ts, 12, 11))
>u : Symbol(u, Decl(destructuringAssignmentWithConstraints.ts, 16, 10))
}

// Test that direct calls work fine (they do)
function testDirectCalls() {
>testDirectCalls : Symbol(testDirectCalls, Decl(destructuringAssignmentWithConstraints.ts, 19, 1))

const result1 = foo({ dataType: 'a', day: 0 });
>result1 : Symbol(result1, Decl(destructuringAssignmentWithConstraints.ts, 23, 7))
>foo : Symbol(foo, Decl(destructuringAssignmentWithConstraints.ts, 2, 26))
>dataType : Symbol(dataType, Decl(destructuringAssignmentWithConstraints.ts, 23, 23))
>day : Symbol(day, Decl(destructuringAssignmentWithConstraints.ts, 23, 38))

const result2 = bar({ dataType: 'a', day: 0 });
>result2 : Symbol(result2, Decl(destructuringAssignmentWithConstraints.ts, 24, 7))
>bar : Symbol(bar, Decl(destructuringAssignmentWithConstraints.ts, 4, 83))
>dataType : Symbol(dataType, Decl(destructuringAssignmentWithConstraints.ts, 24, 23))
>day : Symbol(day, Decl(destructuringAssignmentWithConstraints.ts, 24, 38))

console.log(result1, result2);
>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
>console : Symbol(console, Decl(lib.dom.d.ts, --, --))
>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
>result1 : Symbol(result1, Decl(destructuringAssignmentWithConstraints.ts, 23, 7))
>result2 : Symbol(result2, Decl(destructuringAssignmentWithConstraints.ts, 24, 7))
}
Loading