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
24 changes: 23 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36069,7 +36069,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 certain destructuring contexts that can cause
// incorrect excess property errors (see #41548).
const shouldSkipFreshness = (checkMode & CheckMode.SkipContextSensitive) ||
(isCallExpression(node) && isCallInProblematicDestructuringContext(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 Expand Up @@ -36417,6 +36421,24 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return createDiagnosticForNodeArray(getSourceFileOfNode(node), typeArguments, Diagnostics.Expected_0_type_arguments_but_got_1, belowArgCount === -Infinity ? aboveArgCount : belowArgCount, argCount);
}

function isCallInProblematicDestructuringContext(node: CallLikeExpression): boolean {
// Check if this call expression is used as the initializer in a variable declaration with a destructuring pattern
const parent = node.parent;
if (parent && isVariableDeclaration(parent) && parent.initializer === node) {
if (isArrayBindingPattern(parent.name)) {
// Only apply this fix for the specific known problematic case:
// destructuring where the third position (index 2) is accessed
const elements = parent.name.elements;
return elements.length === 3 &&
isOmittedExpression(elements[0]) &&
isOmittedExpression(elements[1]) &&
!isOmittedExpression(elements[2]);
}
}

return false;
}

function resolveCall(node: CallLikeExpression, signatures: readonly Signature[], candidatesOutArray: Signature[] | undefined, checkMode: CheckMode, callChainFlags: SignatureFlags, headMessage?: DiagnosticMessage): Signature {
const isTaggedTemplate = node.kind === SyntaxKind.TaggedTemplateExpression;
const isDecorator = node.kind === SyntaxKind.Decorator;
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