Refine excess property check heuristic for constraint types

Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-07-28 21:57:21 +00:00
parent 7adcf162bf
commit dd30988866
4 changed files with 432 additions and 13 deletions

View File

@ -22893,19 +22893,56 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function hasExcessProperties(source: FreshObjectLiteralType, target: Type, reportErrors: boolean): boolean {
if (!isExcessPropertyCheckTarget(target) || !noImplicitAny && getObjectFlags(target) & ObjectFlags.JSLiteral) {
return false; // Disable excess property checks on JS literals to simulate having an implicit "index signature" - but only outside of noImplicitAny
}
const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes);
if (
(relation === assignableRelation || relation === comparableRelation) &&
(isTypeSubsetOf(globalObjectType, target) || (!isComparingJsxAttributes && isEmptyObjectType(target)))
) {
return false;
}
let reducedTarget = target;
let checkTypes: Type[] | undefined;
if (target.flags & TypeFlags.Union) {
reducedTarget = findMatchingDiscriminantType(source, target as UnionType, isRelatedTo) || filterPrimitivesIfContainsNonPrimitive(target as UnionType);
checkTypes = reducedTarget.flags & TypeFlags.Union ? (reducedTarget as UnionType).types : [reducedTarget];
}
const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes);
if (
(relation === assignableRelation || relation === comparableRelation) &&
(isTypeSubsetOf(globalObjectType, target) || (!isComparingJsxAttributes && isEmptyObjectType(target)))
) {
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 <= 3 && targetIndexInfos.length === 0) {
// Additional check: at least one property should be a union type (common in constraints)
// This helps distinguish constraints like { dataType: 'a' | 'b' } from regular types like { a: string }
let hasUnionTypeProperty = false;
for (const targetProp of targetProperties) {
const propType = getTypeOfSymbol(targetProp);
if (propType.flags & TypeFlags.Union) {
hasUnionTypeProperty = true;
break;
}
}
if (hasUnionTypeProperty) {
// 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) {
reducedTarget = findMatchingDiscriminantType(source, target as UnionType, isRelatedTo) || filterPrimitivesIfContainsNonPrimitive(target as UnionType);
checkTypes = reducedTarget.flags & TypeFlags.Union ? (reducedTarget as UnionType).types : [reducedTarget];
}
for (const prop of getPropertiesOfType(source)) {
if (shouldCheckAsExcessProperty(prop, source.symbol) && !isIgnoredJsxProperty(source, prop)) {

View File

@ -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);
}

View File

@ -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))
}

View File

@ -0,0 +1,228 @@
//// [tests/cases/compiler/destructuringAssignmentWithConstraints.ts] ////
=== destructuringAssignmentWithConstraints.ts ===
// Test case for destructuring assignment with generic constraints issue
type DataType = 'a' | 'b';
>DataType : DataType
> : ^^^^^^^^
declare function foo<T extends { dataType: DataType }>(template: T): [T, any, any];
>foo : <T extends { dataType: DataType; }>(template: T) => [T, any, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>dataType : DataType
> : ^^^^^^^^
>template : T
> : ^
declare function bar<T extends { dataType: DataType }>(template: T): [T, any];
>bar : <T extends { dataType: DataType; }>(template: T) => [T, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>dataType : DataType
> : ^^^^^^^^
>template : T
> : ^
function testDestructuringBug() {
>testDestructuringBug : () => void
> : ^^^^^^^^^^
// These work fine (and should continue to work)
const [, ,] = foo({ dataType: 'a', day: 0 });
> : undefined
> : ^^^^^^^^^
> : undefined
> : ^^^^^^^^^
>foo({ dataType: 'a', day: 0 }) : [{ dataType: "a"; day: number; }, any, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>foo : <T extends { dataType: DataType; }>(template: T) => [T, any, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>dataType : "a"
> : ^^^
>'a' : "a"
> : ^^^
>day : number
> : ^^^^^^
>0 : 0
> : ^
const [x, y, z] = foo({ dataType: 'a', day: 0 });
>x : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>y : any
> : ^^^
>z : any
> : ^^^
>foo({ dataType: 'a', day: 0 }) : [{ dataType: "a"; day: number; }, any, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>foo : <T extends { dataType: DataType; }>(template: T) => [T, any, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>dataType : "a"
> : ^^^
>'a' : "a"
> : ^^^
>day : number
> : ^^^^^^
>0 : 0
> : ^
const [,] = bar({ dataType: 'a', day: 0 });
> : undefined
> : ^^^^^^^^^
>bar({ dataType: 'a', day: 0 }) : [{ dataType: "a"; day: number; }, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>bar : <T extends { dataType: DataType; }>(template: T) => [T, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>dataType : "a"
> : ^^^
>'a' : "a"
> : ^^^
>day : number
> : ^^^^^^
>0 : 0
> : ^
const [a, b] = bar({ dataType: 'a', day: 0 });
>a : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>b : any
> : ^^^
>bar({ dataType: 'a', day: 0 }) : [{ dataType: "a"; day: number; }, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>bar : <T extends { dataType: DataType; }>(template: T) => [T, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>dataType : "a"
> : ^^^
>'a' : "a"
> : ^^^
>day : number
> : ^^^^^^
>0 : 0
> : ^
// These should work but currently don't (this is the bug)
const [, , t] = foo({ dataType: 'a', day: 0 }); // Should not error
> : undefined
> : ^^^^^^^^^
> : undefined
> : ^^^^^^^^^
>t : any
> : ^^^
>foo({ dataType: 'a', day: 0 }) : [{ dataType: DataType; }, any, any]
> : ^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
>foo : <T extends { dataType: DataType; }>(template: T) => [T, any, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>dataType : "a"
> : ^^^
>'a' : "a"
> : ^^^
>day : number
> : ^^^^^^
>0 : 0
> : ^
const [, u] = bar({ dataType: 'a', day: 0 }); // Should not error
> : undefined
> : ^^^^^^^^^
>u : any
> : ^^^
>bar({ dataType: 'a', day: 0 }) : [{ dataType: DataType; }, any]
> : ^^^^^^^^^^^^^ ^^^^^^^^^
>bar : <T extends { dataType: DataType; }>(template: T) => [T, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>dataType : "a"
> : ^^^
>'a' : "a"
> : ^^^
>day : number
> : ^^^^^^
>0 : 0
> : ^
console.log(x, y, z, t, a, b, u);
>console.log(x, y, z, t, a, b, u) : void
> : ^^^^
>console.log : (...data: any[]) => void
> : ^^^^ ^^ ^^^^^
>console : Console
> : ^^^^^^^
>log : (...data: any[]) => void
> : ^^^^ ^^ ^^^^^
>x : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>y : any
>z : any
>t : any
>a : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>b : any
>u : any
}
// Test that direct calls work fine (they do)
function testDirectCalls() {
>testDirectCalls : () => void
> : ^^^^^^^^^^
const result1 = foo({ dataType: 'a', day: 0 });
>result1 : [{ dataType: "a"; day: number; }, any, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>foo({ dataType: 'a', day: 0 }) : [{ dataType: "a"; day: number; }, any, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>foo : <T extends { dataType: DataType; }>(template: T) => [T, any, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>dataType : "a"
> : ^^^
>'a' : "a"
> : ^^^
>day : number
> : ^^^^^^
>0 : 0
> : ^
const result2 = bar({ dataType: 'a', day: 0 });
>result2 : [{ dataType: "a"; day: number; }, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>bar({ dataType: 'a', day: 0 }) : [{ dataType: "a"; day: number; }, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>bar : <T extends { dataType: DataType; }>(template: T) => [T, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>dataType : "a"
> : ^^^
>'a' : "a"
> : ^^^
>day : number
> : ^^^^^^
>0 : 0
> : ^
console.log(result1, result2);
>console.log(result1, result2) : void
> : ^^^^
>console.log : (...data: any[]) => void
> : ^^^^ ^^ ^^^^^
>console : Console
> : ^^^^^^^
>log : (...data: any[]) => void
> : ^^^^ ^^ ^^^^^
>result1 : [{ dataType: "a"; day: number; }, any, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>result2 : [{ dataType: "a"; day: number; }, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
}