Skip to content

Fix excess property checking in array destructuring contexts #62102

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

Closed
wants to merge 9 commits into from
Closed
25 changes: 23 additions & 2 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31860,7 +31860,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (parent.name.kind === SyntaxKind.ArrayBindingPattern) {
const index = indexOfNode(declaration.parent.elements, declaration);
if (index < 0) return undefined;
return getContextualTypeForElementExpression(parentType, index);
return getContextualTypeForElementExpression(parentType, index, declaration.parent.elements.length);
}
const nameType = getLiteralTypeFromPropertyName(name);
if (isTypeUsableAsPropertyName(nameType)) {
Expand Down Expand Up @@ -36027,6 +36027,23 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return skipOuterExpressions(argument, flags);
}

function isCallInLastTupleElementDestructuring(node: CallLikeExpression): boolean {
// Check if this call expression is used as initializer in a variable declaration with array destructuring
const parent = node.parent;
if (parent && isVariableDeclaration(parent) && parent.initializer === node && isArrayBindingPattern(parent.name)) {
const elements = parent.name.elements;
// Check if the destructuring pattern accesses the last element
// (i.e., the last non-omitted element is at the end of the pattern)
for (let i = elements.length - 1; i >= 0; i--) {
if (!isOmittedExpression(elements[i])) {
// If the last non-omitted element is at the last position, it's accessing the last tuple element
return i === elements.length - 1;
}
}
}
return false;
}

function getSignatureApplicabilityError(
node: CallLikeExpression,
args: readonly Expression[],
Expand Down Expand Up @@ -36069,7 +36086,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// If one or more arguments are still excluded (as indicated by CheckMode.SkipContextSensitive),
// we obtain the regular type of any object literal arguments because we may not have inferred complete
// parameter types yet and therefore excess property checks may yield false positives (see #17041).
const checkArgType = checkMode & CheckMode.SkipContextSensitive ? getRegularTypeOfObjectLiteral(argType) : argType;
// Also skip fresh literal checking when the call is in last tuple element destructuring context
// to prevent incorrect excess property errors (see #41548).
const shouldSkipFreshness = (checkMode & CheckMode.SkipContextSensitive) ||
(isCallExpression(node) && isCallInLastTupleElementDestructuring(node));
const checkArgType = shouldSkipFreshness ? getRegularTypeOfObjectLiteral(argType) : argType;
const effectiveCheckArgumentNode = getEffectiveCheckNode(arg);
if (!checkTypeRelatedToAndOptionallyElaborate(checkArgType, paramType, relation, reportErrors ? effectiveCheckArgumentNode : undefined, effectiveCheckArgumentNode, headMessage, containingMessageChain, errorOutputContainer)) {
Debug.assert(!reportErrors || !!errorOutputContainer.errors, "parameter should have errors when reporting errors");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
excessPropertyCheckingInArrayDestructuring.ts(12,28): error TS2322: Type '"c"' is not assignable to type '"a" | "b"'.
excessPropertyCheckingInArrayDestructuring.ts(13,26): error TS2345: Argument of type 'number' is not assignable to parameter of type '{ dataType: "a" | "b"; }'.


==== excessPropertyCheckingInArrayDestructuring.ts (2 errors) ====
declare function foo<T extends { dataType: 'a' | 'b' }>(template: T): [T, any, any];

// Test the specific problematic case that should now work
const [, , works1] = foo({ dataType: 'a', day: 0 });
const [, , works2] = foo({ dataType: 'b', extra: 'value' });

// Test assignment destructuring (not currently fixed)
let a: any;
[, , a] = foo({ dataType: 'a', day: 0 }); // This might still error

// Test that legitimate errors are still caught
const [, , fails1] = foo({ dataType: 'c' }); // Error: 'c' not assignable to 'a' | 'b'
~~~~~~~~
!!! error TS2322: Type '"c"' is not assignable to type '"a" | "b"'.
!!! related TS6500 excessPropertyCheckingInArrayDestructuring.ts:1:34: The expected type comes from property 'dataType' which is declared here on type '{ dataType: "a" | "b"; }'
const [, , fails2] = foo(123); // Error: number not assignable to constraint
~~~
!!! error TS2345: Argument of type 'number' is not assignable to parameter of type '{ dataType: "a" | "b"; }'.

// Test that non-destructuring cases work as before
const result = foo({ dataType: 'a', day: 0 }); // Should work
const explicit: [{ dataType: 'a', day: number }, any, any] = foo({ dataType: 'a', day: 0 }); // Should work

// Test that other destructuring patterns work correctly
const [first] = foo({ dataType: 'a', day: 0 }); // Should work
const [, second] = foo({ dataType: 'a', day: 0 }); // Should work
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//// [tests/cases/compiler/excessPropertyCheckingInArrayDestructuring.ts] ////

//// [excessPropertyCheckingInArrayDestructuring.ts]
declare function foo<T extends { dataType: 'a' | 'b' }>(template: T): [T, any, any];

// Test the specific problematic case that should now work
const [, , works1] = foo({ dataType: 'a', day: 0 });
const [, , works2] = foo({ dataType: 'b', extra: 'value' });

// Test assignment destructuring (not currently fixed)
let a: any;
[, , a] = foo({ dataType: 'a', day: 0 }); // This might still error

// Test that legitimate errors are still caught
const [, , fails1] = foo({ dataType: 'c' }); // Error: 'c' not assignable to 'a' | 'b'
const [, , fails2] = foo(123); // Error: number not assignable to constraint

// Test that non-destructuring cases work as before
const result = foo({ dataType: 'a', day: 0 }); // Should work
const explicit: [{ dataType: 'a', day: number }, any, any] = foo({ dataType: 'a', day: 0 }); // Should work

// Test that other destructuring patterns work correctly
const [first] = foo({ dataType: 'a', day: 0 }); // Should work
const [, second] = foo({ dataType: 'a', day: 0 }); // Should work

//// [excessPropertyCheckingInArrayDestructuring.js]
"use strict";
var _a;
// Test the specific problematic case that should now work
var _b = foo({ dataType: 'a', day: 0 }), works1 = _b[2];
var _c = foo({ dataType: 'b', extra: 'value' }), works2 = _c[2];
// Test assignment destructuring (not currently fixed)
var a;
_a = foo({ dataType: 'a', day: 0 }), a = _a[2]; // This might still error
// Test that legitimate errors are still caught
var _d = foo({ dataType: 'c' }), fails1 = _d[2]; // Error: 'c' not assignable to 'a' | 'b'
var _e = foo(123), fails2 = _e[2]; // Error: number not assignable to constraint
// Test that non-destructuring cases work as before
var result = foo({ dataType: 'a', day: 0 }); // Should work
var explicit = foo({ dataType: 'a', day: 0 }); // Should work
// Test that other destructuring patterns work correctly
var first = foo({ dataType: 'a', day: 0 })[0]; // Should work
var _f = foo({ dataType: 'a', day: 0 }), second = _f[1]; // Should work
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//// [tests/cases/compiler/excessPropertyCheckingInArrayDestructuring.ts] ////

=== excessPropertyCheckingInArrayDestructuring.ts ===
declare function foo<T extends { dataType: 'a' | 'b' }>(template: T): [T, any, any];
>foo : Symbol(foo, Decl(excessPropertyCheckingInArrayDestructuring.ts, 0, 0))
>T : Symbol(T, Decl(excessPropertyCheckingInArrayDestructuring.ts, 0, 21))
>dataType : Symbol(dataType, Decl(excessPropertyCheckingInArrayDestructuring.ts, 0, 32))
>template : Symbol(template, Decl(excessPropertyCheckingInArrayDestructuring.ts, 0, 56))
>T : Symbol(T, Decl(excessPropertyCheckingInArrayDestructuring.ts, 0, 21))
>T : Symbol(T, Decl(excessPropertyCheckingInArrayDestructuring.ts, 0, 21))

// Test the specific problematic case that should now work
const [, , works1] = foo({ dataType: 'a', day: 0 });
>works1 : Symbol(works1, Decl(excessPropertyCheckingInArrayDestructuring.ts, 3, 10))
>foo : Symbol(foo, Decl(excessPropertyCheckingInArrayDestructuring.ts, 0, 0))
>dataType : Symbol(dataType, Decl(excessPropertyCheckingInArrayDestructuring.ts, 3, 26))
>day : Symbol(day, Decl(excessPropertyCheckingInArrayDestructuring.ts, 3, 41))

const [, , works2] = foo({ dataType: 'b', extra: 'value' });
>works2 : Symbol(works2, Decl(excessPropertyCheckingInArrayDestructuring.ts, 4, 10))
>foo : Symbol(foo, Decl(excessPropertyCheckingInArrayDestructuring.ts, 0, 0))
>dataType : Symbol(dataType, Decl(excessPropertyCheckingInArrayDestructuring.ts, 4, 26))
>extra : Symbol(extra, Decl(excessPropertyCheckingInArrayDestructuring.ts, 4, 41))

// Test assignment destructuring (not currently fixed)
let a: any;
>a : Symbol(a, Decl(excessPropertyCheckingInArrayDestructuring.ts, 7, 3))

[, , a] = foo({ dataType: 'a', day: 0 }); // This might still error
>a : Symbol(a, Decl(excessPropertyCheckingInArrayDestructuring.ts, 7, 3))
>foo : Symbol(foo, Decl(excessPropertyCheckingInArrayDestructuring.ts, 0, 0))
>dataType : Symbol(dataType, Decl(excessPropertyCheckingInArrayDestructuring.ts, 8, 15))
>day : Symbol(day, Decl(excessPropertyCheckingInArrayDestructuring.ts, 8, 30))

// Test that legitimate errors are still caught
const [, , fails1] = foo({ dataType: 'c' }); // Error: 'c' not assignable to 'a' | 'b'
>fails1 : Symbol(fails1, Decl(excessPropertyCheckingInArrayDestructuring.ts, 11, 10))
>foo : Symbol(foo, Decl(excessPropertyCheckingInArrayDestructuring.ts, 0, 0))
>dataType : Symbol(dataType, Decl(excessPropertyCheckingInArrayDestructuring.ts, 11, 26))

const [, , fails2] = foo(123); // Error: number not assignable to constraint
>fails2 : Symbol(fails2, Decl(excessPropertyCheckingInArrayDestructuring.ts, 12, 10))
>foo : Symbol(foo, Decl(excessPropertyCheckingInArrayDestructuring.ts, 0, 0))

// Test that non-destructuring cases work as before
const result = foo({ dataType: 'a', day: 0 }); // Should work
>result : Symbol(result, Decl(excessPropertyCheckingInArrayDestructuring.ts, 15, 5))
>foo : Symbol(foo, Decl(excessPropertyCheckingInArrayDestructuring.ts, 0, 0))
>dataType : Symbol(dataType, Decl(excessPropertyCheckingInArrayDestructuring.ts, 15, 20))
>day : Symbol(day, Decl(excessPropertyCheckingInArrayDestructuring.ts, 15, 35))

const explicit: [{ dataType: 'a', day: number }, any, any] = foo({ dataType: 'a', day: 0 }); // Should work
>explicit : Symbol(explicit, Decl(excessPropertyCheckingInArrayDestructuring.ts, 16, 5))
>dataType : Symbol(dataType, Decl(excessPropertyCheckingInArrayDestructuring.ts, 16, 18))
>day : Symbol(day, Decl(excessPropertyCheckingInArrayDestructuring.ts, 16, 33))
>foo : Symbol(foo, Decl(excessPropertyCheckingInArrayDestructuring.ts, 0, 0))
>dataType : Symbol(dataType, Decl(excessPropertyCheckingInArrayDestructuring.ts, 16, 66))
>day : Symbol(day, Decl(excessPropertyCheckingInArrayDestructuring.ts, 16, 81))

// Test that other destructuring patterns work correctly
const [first] = foo({ dataType: 'a', day: 0 }); // Should work
>first : Symbol(first, Decl(excessPropertyCheckingInArrayDestructuring.ts, 19, 7))
>foo : Symbol(foo, Decl(excessPropertyCheckingInArrayDestructuring.ts, 0, 0))
>dataType : Symbol(dataType, Decl(excessPropertyCheckingInArrayDestructuring.ts, 19, 21))
>day : Symbol(day, Decl(excessPropertyCheckingInArrayDestructuring.ts, 19, 36))

const [, second] = foo({ dataType: 'a', day: 0 }); // Should work
>second : Symbol(second, Decl(excessPropertyCheckingInArrayDestructuring.ts, 20, 8))
>foo : Symbol(foo, Decl(excessPropertyCheckingInArrayDestructuring.ts, 0, 0))
>dataType : Symbol(dataType, Decl(excessPropertyCheckingInArrayDestructuring.ts, 20, 24))
>day : Symbol(day, Decl(excessPropertyCheckingInArrayDestructuring.ts, 20, 39))

Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
//// [tests/cases/compiler/excessPropertyCheckingInArrayDestructuring.ts] ////

=== excessPropertyCheckingInArrayDestructuring.ts ===
declare function foo<T extends { dataType: 'a' | 'b' }>(template: T): [T, any, any];
>foo : <T extends { dataType: "a" | "b"; }>(template: T) => [T, any, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>dataType : "a" | "b"
> : ^^^^^^^^^
>template : T
> : ^

// Test the specific problematic case that should now work
const [, , works1] = foo({ dataType: 'a', day: 0 });
> : undefined
> : ^^^^^^^^^
> : undefined
> : ^^^^^^^^^
>works1 : any
> : ^^^
>foo({ dataType: 'a', day: 0 }) : [{ dataType: "a" | "b"; }, any, any]
> : ^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
>foo : <T extends { dataType: "a" | "b"; }>(template: T) => [T, any, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>dataType : "a"
> : ^^^
>'a' : "a"
> : ^^^
>day : number
> : ^^^^^^
>0 : 0
> : ^

const [, , works2] = foo({ dataType: 'b', extra: 'value' });
> : undefined
> : ^^^^^^^^^
> : undefined
> : ^^^^^^^^^
>works2 : any
> : ^^^
>foo({ dataType: 'b', extra: 'value' }) : [{ dataType: "a" | "b"; }, any, any]
> : ^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
>foo : <T extends { dataType: "a" | "b"; }>(template: T) => [T, any, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>{ dataType: 'b', extra: 'value' } : { dataType: "b"; extra: string; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>dataType : "b"
> : ^^^
>'b' : "b"
> : ^^^
>extra : string
> : ^^^^^^
>'value' : "value"
> : ^^^^^^^

// Test assignment destructuring (not currently fixed)
let a: any;
>a : any
> : ^^^

[, , a] = foo({ dataType: 'a', day: 0 }); // This might still error
>[, , a] = foo({ dataType: 'a', day: 0 }) : [{ dataType: "a"; day: number; }, any, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>[, , a] : [undefined, undefined, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^
> : undefined
> : ^^^^^^^^^
> : undefined
> : ^^^^^^^^^
>a : any
> : ^^^
>foo({ dataType: 'a', day: 0 }) : [{ dataType: "a"; day: number; }, any, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>foo : <T extends { dataType: "a" | "b"; }>(template: T) => [T, any, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>dataType : "a"
> : ^^^
>'a' : "a"
> : ^^^
>day : number
> : ^^^^^^
>0 : 0
> : ^

// Test that legitimate errors are still caught
const [, , fails1] = foo({ dataType: 'c' }); // Error: 'c' not assignable to 'a' | 'b'
> : undefined
> : ^^^^^^^^^
> : undefined
> : ^^^^^^^^^
>fails1 : any
> : ^^^
>foo({ dataType: 'c' }) : [{ dataType: "a" | "b"; }, any, any]
> : ^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
>foo : <T extends { dataType: "a" | "b"; }>(template: T) => [T, any, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>{ dataType: 'c' } : { dataType: "c"; }
> : ^^^^^^^^^^^^^^^^^^
>dataType : "c"
> : ^^^
>'c' : "c"
> : ^^^

const [, , fails2] = foo(123); // Error: number not assignable to constraint
> : undefined
> : ^^^^^^^^^
> : undefined
> : ^^^^^^^^^
>fails2 : any
> : ^^^
>foo(123) : [{ dataType: "a" | "b"; }, any, any]
> : ^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
>foo : <T extends { dataType: "a" | "b"; }>(template: T) => [T, any, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>123 : 123
> : ^^^

// Test that non-destructuring cases work as before
const result = foo({ dataType: 'a', day: 0 }); // Should work
>result : [{ dataType: "a"; day: number; }, any, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>foo({ dataType: 'a', day: 0 }) : [{ dataType: "a"; day: number; }, any, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>foo : <T extends { dataType: "a" | "b"; }>(template: T) => [T, any, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>dataType : "a"
> : ^^^
>'a' : "a"
> : ^^^
>day : number
> : ^^^^^^
>0 : 0
> : ^

const explicit: [{ dataType: 'a', day: number }, any, any] = foo({ dataType: 'a', day: 0 }); // Should work
>explicit : [{ dataType: "a"; day: number; }, any, any]
> : ^^^^^^^^^^^^^ ^^^^^^^ ^^^^^^^^^^^^^^
>dataType : "a"
> : ^^^
>day : number
> : ^^^^^^
>foo({ dataType: 'a', day: 0 }) : [{ dataType: "a"; day: number; }, any, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>foo : <T extends { dataType: "a" | "b"; }>(template: T) => [T, any, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>dataType : "a"
> : ^^^
>'a' : "a"
> : ^^^
>day : number
> : ^^^^^^
>0 : 0
> : ^

// Test that other destructuring patterns work correctly
const [first] = foo({ dataType: 'a', day: 0 }); // Should work
>first : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>foo({ dataType: 'a', day: 0 }) : [{ dataType: "a"; day: number; }, any, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>foo : <T extends { dataType: "a" | "b"; }>(template: T) => [T, any, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>dataType : "a"
> : ^^^
>'a' : "a"
> : ^^^
>day : number
> : ^^^^^^
>0 : 0
> : ^

const [, second] = foo({ dataType: 'a', day: 0 }); // Should work
> : undefined
> : ^^^^^^^^^
>second : any
> : ^^^
>foo({ dataType: 'a', day: 0 }) : [{ dataType: "a"; day: number; }, any, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>foo : <T extends { dataType: "a" | "b"; }>(template: T) => [T, any, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>dataType : "a"
> : ^^^
>'a' : "a"
> : ^^^
>day : number
> : ^^^^^^
>0 : 0
> : ^

Loading
Loading