From dd265074c64543b112e1c57c6753449bd68d7581 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 21 Jul 2025 22:16:09 +0000 Subject: [PATCH] Fix excess property checking when accessing last tuple element in destructuring Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- src/compiler/checker.ts | 69 +++-- .../lastTupleElementDestructuring.errors.txt | 52 ++++ .../lastTupleElementDestructuring.js | 55 ++++ .../lastTupleElementDestructuring.symbols | 103 ++++++++ .../lastTupleElementDestructuring.types | 248 ++++++++++++++++++ .../compiler/lastTupleElementDestructuring.ts | 34 ++- tests/debug_test.ts | 8 - 7 files changed, 531 insertions(+), 38 deletions(-) create mode 100644 tests/baselines/reference/lastTupleElementDestructuring.errors.txt create mode 100644 tests/baselines/reference/lastTupleElementDestructuring.js create mode 100644 tests/baselines/reference/lastTupleElementDestructuring.symbols create mode 100644 tests/baselines/reference/lastTupleElementDestructuring.types delete mode 100644 tests/debug_test.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 519b724fb62..23e0d910719 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -31857,10 +31857,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const parentType = getContextualTypeForVariableLikeDeclaration(parent, contextFlags) || parent.kind !== SyntaxKind.BindingElement && parent.initializer && checkDeclarationInitializer(parent, declaration.dotDotDotToken ? CheckMode.RestBindingElement : CheckMode.Normal); if (!parentType || isBindingPattern(name) || isComputedNonLiteralName(name)) return undefined; - if (parent.name.kind === SyntaxKind.ArrayBindingPattern) { - const index = indexOfNode(declaration.parent.elements, declaration); - if (index < 0) return undefined; - return getContextualTypeForElementExpression(parentType, index, declaration.parent.elements.length); + if (parent.name.kind === SyntaxKind.ArrayBindingPattern) { + const index = indexOfNode(declaration.parent.elements, declaration); + if (index < 0) return undefined; + return getContextualTypeForElementExpression(parentType, index, declaration.parent.elements.length); } const nameType = getLiteralTypeFromPropertyName(name); if (isTypeUsableAsPropertyName(nameType)) { @@ -32391,25 +32391,25 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return { first, last }; } - function getContextualTypeForElementExpression(type: Type | undefined, index: number, length?: number, firstSpreadIndex?: number, lastSpreadIndex?: number): Type | undefined { - return type && mapType(type, t => { - if (isTupleType(t)) { - // If index is before any spread element and within the fixed part of the contextual tuple type, return - // the type of the contextual tuple element. - if ((firstSpreadIndex === undefined || index < firstSpreadIndex) && index < t.target.fixedLength) { - return removeMissingType(getTypeArguments(t)[index], !!(t.target.elementFlags[index] && ElementFlags.Optional)); - } - // When the length is known and the index is after all spread elements we compute the offset from the element - // to the end and the number of ending fixed elements in the contextual tuple type. - const offset = length !== undefined && (lastSpreadIndex === undefined || index > lastSpreadIndex) ? length - index : 0; - const fixedEndLength = offset > 0 && (t.target.combinedFlags & ElementFlags.Variable) ? getEndElementCount(t.target, ElementFlags.Fixed) : 0; - // If the offset is within the ending fixed part of the contextual tuple type, return the type of the contextual - // tuple element. - if (offset > 0 && offset <= fixedEndLength) { - return getTypeArguments(t)[getTypeReferenceArity(t) - offset]; - } - // Return a union of the possible contextual element types with no subtype reduction. - return getElementTypeOfSliceOfTupleType(t, firstSpreadIndex === undefined ? t.target.fixedLength : Math.min(t.target.fixedLength, firstSpreadIndex), length === undefined || lastSpreadIndex === undefined ? fixedEndLength : Math.min(fixedEndLength, length - lastSpreadIndex), /*writing*/ false, /*noReductions*/ true); + function getContextualTypeForElementExpression(type: Type | undefined, index: number, length?: number, firstSpreadIndex?: number, lastSpreadIndex?: number): Type | undefined { + return type && mapType(type, t => { + if (isTupleType(t)) { + // If index is before any spread element and within the fixed part of the contextual tuple type, return + // the type of the contextual tuple element. + if ((firstSpreadIndex === undefined || index < firstSpreadIndex) && index < t.target.fixedLength) { + return removeMissingType(getTypeArguments(t)[index], !!(t.target.elementFlags[index] && ElementFlags.Optional)); + } + // When the length is known and the index is after all spread elements we compute the offset from the element + // to the end and the number of ending fixed elements in the contextual tuple type. + const offset = length !== undefined && (lastSpreadIndex === undefined || index > lastSpreadIndex) ? length - index : 0; + const fixedEndLength = offset > 0 && (t.target.combinedFlags & ElementFlags.Variable) ? getEndElementCount(t.target, ElementFlags.Fixed) : 0; + // If the offset is within the ending fixed part of the contextual tuple type, return the type of the contextual + // tuple element. + if (offset > 0 && offset <= fixedEndLength) { + return getTypeArguments(t)[getTypeReferenceArity(t) - offset]; + } + // Return a union of the possible contextual element types with no subtype reduction. + return getElementTypeOfSliceOfTupleType(t, firstSpreadIndex === undefined ? t.target.fixedLength : Math.min(t.target.fixedLength, firstSpreadIndex), length === undefined || lastSpreadIndex === undefined ? fixedEndLength : Math.min(fixedEndLength, length - lastSpreadIndex), /*writing*/ false, /*noReductions*/ true); } // If element index is known and a contextual property with that name exists, return it. Otherwise return the // iterated or element type of the contextual type. @@ -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[], @@ -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"); diff --git a/tests/baselines/reference/lastTupleElementDestructuring.errors.txt b/tests/baselines/reference/lastTupleElementDestructuring.errors.txt new file mode 100644 index 00000000000..882a2092a40 --- /dev/null +++ b/tests/baselines/reference/lastTupleElementDestructuring.errors.txt @@ -0,0 +1,52 @@ +lastTupleElementDestructuring.ts(18,27): error TS2322: Type '"c"' is not assignable to type '"a" | "b"'. +lastTupleElementDestructuring.ts(19,24): error TS2345: Argument of type '{ notDataType: string; }' is not assignable to parameter of type '{ dataType: "a" | "b"; }'. + Property 'dataType' is missing in type '{ notDataType: string; }' but required in type '{ dataType: "a" | "b"; }'. +lastTupleElementDestructuring.ts(33,34): error TS2345: Argument of type '{ optional: number; }' is not assignable to parameter of type 'Config'. + Property 'required' is missing in type '{ optional: number; }' but required in type 'Config'. + + +==== lastTupleElementDestructuring.ts (3 errors) ==== + // Test for fixing excess property checking when accessing last tuple element in destructuring + // Fixes https://github.com/microsoft/TypeScript/issues/41548 + + declare function foo(template: T): [T, any, any]; + declare function bar(template: T): [T, any, any, any]; + + // Cases that should NOT error after fix (accessing last element) + const [, , last1] = foo({ dataType: 'a', day: 0 }); + const [,,last2] = foo({ dataType: 'a', day: 0 }); + const [,,,last3] = bar({ dataType: 'a', day: 0 }); + + // Cases that already worked (not accessing last element) + const [, mid1, ] = foo({ dataType: 'a', day: 0 }); + const [first1, , ] = foo({ dataType: 'a', day: 0 }); + const [,,third,] = bar({ dataType: 'a', day: 0 }); + + // Legitimate errors should still be caught + const [, , last4] = foo({ dataType: 'c' }); // Error: 'c' not assignable to 'a' | 'b' + ~~~~~~~~ +!!! error TS2322: Type '"c"' is not assignable to type '"a" | "b"'. +!!! related TS6500 lastTupleElementDestructuring.ts:4:34: The expected type comes from property 'dataType' which is declared here on type '{ dataType: "a" | "b"; }' + const [,,,last5] = bar({ notDataType: 'a' }); // Error: missing required property 'dataType' + ~~~~~~~~~~~~~~~~~~~~ +!!! error TS2345: Argument of type '{ notDataType: string; }' is not assignable to parameter of type '{ dataType: "a" | "b"; }'. +!!! error TS2345: Property 'dataType' is missing in type '{ notDataType: string; }' but required in type '{ dataType: "a" | "b"; }'. +!!! related TS2728 lastTupleElementDestructuring.ts:5:34: 'dataType' is declared here. + + // Test with more complex object properties + interface Config { + required: string; + optional?: number; + } + + declare function withConfig(template: T): [T, string]; + + // Should work - accessing last element with extra property + const [,configStr] = withConfig({ required: 'test', extra: 'should work' }); + + // Should still error - missing required property + const [,configStr2] = withConfig({ optional: 42 }); // Error: missing 'required' + ~~~~~~~~~~~~~~~~ +!!! error TS2345: Argument of type '{ optional: number; }' is not assignable to parameter of type 'Config'. +!!! error TS2345: Property 'required' is missing in type '{ optional: number; }' but required in type 'Config'. +!!! related TS2728 lastTupleElementDestructuring.ts:23:5: 'required' is declared here. \ No newline at end of file diff --git a/tests/baselines/reference/lastTupleElementDestructuring.js b/tests/baselines/reference/lastTupleElementDestructuring.js new file mode 100644 index 00000000000..dcc1ea56bcd --- /dev/null +++ b/tests/baselines/reference/lastTupleElementDestructuring.js @@ -0,0 +1,55 @@ +//// [tests/cases/compiler/lastTupleElementDestructuring.ts] //// + +//// [lastTupleElementDestructuring.ts] +// Test for fixing excess property checking when accessing last tuple element in destructuring +// Fixes https://github.com/microsoft/TypeScript/issues/41548 + +declare function foo(template: T): [T, any, any]; +declare function bar(template: T): [T, any, any, any]; + +// Cases that should NOT error after fix (accessing last element) +const [, , last1] = foo({ dataType: 'a', day: 0 }); +const [,,last2] = foo({ dataType: 'a', day: 0 }); +const [,,,last3] = bar({ dataType: 'a', day: 0 }); + +// Cases that already worked (not accessing last element) +const [, mid1, ] = foo({ dataType: 'a', day: 0 }); +const [first1, , ] = foo({ dataType: 'a', day: 0 }); +const [,,third,] = bar({ dataType: 'a', day: 0 }); + +// Legitimate errors should still be caught +const [, , last4] = foo({ dataType: 'c' }); // Error: 'c' not assignable to 'a' | 'b' +const [,,,last5] = bar({ notDataType: 'a' }); // Error: missing required property 'dataType' + +// Test with more complex object properties +interface Config { + required: string; + optional?: number; +} + +declare function withConfig(template: T): [T, string]; + +// Should work - accessing last element with extra property +const [,configStr] = withConfig({ required: 'test', extra: 'should work' }); + +// Should still error - missing required property +const [,configStr2] = withConfig({ optional: 42 }); // Error: missing 'required' + +//// [lastTupleElementDestructuring.js] +// Test for fixing excess property checking when accessing last tuple element in destructuring +// Fixes https://github.com/microsoft/TypeScript/issues/41548 +// Cases that should NOT error after fix (accessing last element) +var _a = foo({ dataType: 'a', day: 0 }), last1 = _a[2]; +var _b = foo({ dataType: 'a', day: 0 }), last2 = _b[2]; +var _c = bar({ dataType: 'a', day: 0 }), last3 = _c[3]; +// Cases that already worked (not accessing last element) +var _d = foo({ dataType: 'a', day: 0 }), mid1 = _d[1]; +var _e = foo({ dataType: 'a', day: 0 }), first1 = _e[0]; +var _f = bar({ dataType: 'a', day: 0 }), third = _f[2]; +// Legitimate errors should still be caught +var _g = foo({ dataType: 'c' }), last4 = _g[2]; // Error: 'c' not assignable to 'a' | 'b' +var _h = bar({ notDataType: 'a' }), last5 = _h[3]; // Error: missing required property 'dataType' +// Should work - accessing last element with extra property +var _j = withConfig({ required: 'test', extra: 'should work' }), configStr = _j[1]; +// Should still error - missing required property +var _k = withConfig({ optional: 42 }), configStr2 = _k[1]; // Error: missing 'required' diff --git a/tests/baselines/reference/lastTupleElementDestructuring.symbols b/tests/baselines/reference/lastTupleElementDestructuring.symbols new file mode 100644 index 00000000000..c264f3464e2 --- /dev/null +++ b/tests/baselines/reference/lastTupleElementDestructuring.symbols @@ -0,0 +1,103 @@ +//// [tests/cases/compiler/lastTupleElementDestructuring.ts] //// + +=== lastTupleElementDestructuring.ts === +// Test for fixing excess property checking when accessing last tuple element in destructuring +// Fixes https://github.com/microsoft/TypeScript/issues/41548 + +declare function foo(template: T): [T, any, any]; +>foo : Symbol(foo, Decl(lastTupleElementDestructuring.ts, 0, 0)) +>T : Symbol(T, Decl(lastTupleElementDestructuring.ts, 3, 21)) +>dataType : Symbol(dataType, Decl(lastTupleElementDestructuring.ts, 3, 32)) +>template : Symbol(template, Decl(lastTupleElementDestructuring.ts, 3, 56)) +>T : Symbol(T, Decl(lastTupleElementDestructuring.ts, 3, 21)) +>T : Symbol(T, Decl(lastTupleElementDestructuring.ts, 3, 21)) + +declare function bar(template: T): [T, any, any, any]; +>bar : Symbol(bar, Decl(lastTupleElementDestructuring.ts, 3, 84)) +>T : Symbol(T, Decl(lastTupleElementDestructuring.ts, 4, 21)) +>dataType : Symbol(dataType, Decl(lastTupleElementDestructuring.ts, 4, 32)) +>template : Symbol(template, Decl(lastTupleElementDestructuring.ts, 4, 56)) +>T : Symbol(T, Decl(lastTupleElementDestructuring.ts, 4, 21)) +>T : Symbol(T, Decl(lastTupleElementDestructuring.ts, 4, 21)) + +// Cases that should NOT error after fix (accessing last element) +const [, , last1] = foo({ dataType: 'a', day: 0 }); +>last1 : Symbol(last1, Decl(lastTupleElementDestructuring.ts, 7, 10)) +>foo : Symbol(foo, Decl(lastTupleElementDestructuring.ts, 0, 0)) +>dataType : Symbol(dataType, Decl(lastTupleElementDestructuring.ts, 7, 25)) +>day : Symbol(day, Decl(lastTupleElementDestructuring.ts, 7, 40)) + +const [,,last2] = foo({ dataType: 'a', day: 0 }); +>last2 : Symbol(last2, Decl(lastTupleElementDestructuring.ts, 8, 9)) +>foo : Symbol(foo, Decl(lastTupleElementDestructuring.ts, 0, 0)) +>dataType : Symbol(dataType, Decl(lastTupleElementDestructuring.ts, 8, 23)) +>day : Symbol(day, Decl(lastTupleElementDestructuring.ts, 8, 38)) + +const [,,,last3] = bar({ dataType: 'a', day: 0 }); +>last3 : Symbol(last3, Decl(lastTupleElementDestructuring.ts, 9, 10)) +>bar : Symbol(bar, Decl(lastTupleElementDestructuring.ts, 3, 84)) +>dataType : Symbol(dataType, Decl(lastTupleElementDestructuring.ts, 9, 24)) +>day : Symbol(day, Decl(lastTupleElementDestructuring.ts, 9, 39)) + +// Cases that already worked (not accessing last element) +const [, mid1, ] = foo({ dataType: 'a', day: 0 }); +>mid1 : Symbol(mid1, Decl(lastTupleElementDestructuring.ts, 12, 8)) +>foo : Symbol(foo, Decl(lastTupleElementDestructuring.ts, 0, 0)) +>dataType : Symbol(dataType, Decl(lastTupleElementDestructuring.ts, 12, 24)) +>day : Symbol(day, Decl(lastTupleElementDestructuring.ts, 12, 39)) + +const [first1, , ] = foo({ dataType: 'a', day: 0 }); +>first1 : Symbol(first1, Decl(lastTupleElementDestructuring.ts, 13, 7)) +>foo : Symbol(foo, Decl(lastTupleElementDestructuring.ts, 0, 0)) +>dataType : Symbol(dataType, Decl(lastTupleElementDestructuring.ts, 13, 26)) +>day : Symbol(day, Decl(lastTupleElementDestructuring.ts, 13, 41)) + +const [,,third,] = bar({ dataType: 'a', day: 0 }); +>third : Symbol(third, Decl(lastTupleElementDestructuring.ts, 14, 9)) +>bar : Symbol(bar, Decl(lastTupleElementDestructuring.ts, 3, 84)) +>dataType : Symbol(dataType, Decl(lastTupleElementDestructuring.ts, 14, 24)) +>day : Symbol(day, Decl(lastTupleElementDestructuring.ts, 14, 39)) + +// Legitimate errors should still be caught +const [, , last4] = foo({ dataType: 'c' }); // Error: 'c' not assignable to 'a' | 'b' +>last4 : Symbol(last4, Decl(lastTupleElementDestructuring.ts, 17, 10)) +>foo : Symbol(foo, Decl(lastTupleElementDestructuring.ts, 0, 0)) +>dataType : Symbol(dataType, Decl(lastTupleElementDestructuring.ts, 17, 25)) + +const [,,,last5] = bar({ notDataType: 'a' }); // Error: missing required property 'dataType' +>last5 : Symbol(last5, Decl(lastTupleElementDestructuring.ts, 18, 10)) +>bar : Symbol(bar, Decl(lastTupleElementDestructuring.ts, 3, 84)) +>notDataType : Symbol(notDataType, Decl(lastTupleElementDestructuring.ts, 18, 24)) + +// Test with more complex object properties +interface Config { +>Config : Symbol(Config, Decl(lastTupleElementDestructuring.ts, 18, 45)) + + required: string; +>required : Symbol(Config.required, Decl(lastTupleElementDestructuring.ts, 21, 18)) + + optional?: number; +>optional : Symbol(Config.optional, Decl(lastTupleElementDestructuring.ts, 22, 21)) +} + +declare function withConfig(template: T): [T, string]; +>withConfig : Symbol(withConfig, Decl(lastTupleElementDestructuring.ts, 24, 1)) +>T : Symbol(T, Decl(lastTupleElementDestructuring.ts, 26, 28)) +>Config : Symbol(Config, Decl(lastTupleElementDestructuring.ts, 18, 45)) +>template : Symbol(template, Decl(lastTupleElementDestructuring.ts, 26, 46)) +>T : Symbol(T, Decl(lastTupleElementDestructuring.ts, 26, 28)) +>T : Symbol(T, Decl(lastTupleElementDestructuring.ts, 26, 28)) + +// Should work - accessing last element with extra property +const [,configStr] = withConfig({ required: 'test', extra: 'should work' }); +>configStr : Symbol(configStr, Decl(lastTupleElementDestructuring.ts, 29, 8)) +>withConfig : Symbol(withConfig, Decl(lastTupleElementDestructuring.ts, 24, 1)) +>required : Symbol(required, Decl(lastTupleElementDestructuring.ts, 29, 33)) +>extra : Symbol(extra, Decl(lastTupleElementDestructuring.ts, 29, 51)) + +// Should still error - missing required property +const [,configStr2] = withConfig({ optional: 42 }); // Error: missing 'required' +>configStr2 : Symbol(configStr2, Decl(lastTupleElementDestructuring.ts, 32, 8)) +>withConfig : Symbol(withConfig, Decl(lastTupleElementDestructuring.ts, 24, 1)) +>optional : Symbol(optional, Decl(lastTupleElementDestructuring.ts, 32, 34)) + diff --git a/tests/baselines/reference/lastTupleElementDestructuring.types b/tests/baselines/reference/lastTupleElementDestructuring.types new file mode 100644 index 00000000000..660e2a3fdea --- /dev/null +++ b/tests/baselines/reference/lastTupleElementDestructuring.types @@ -0,0 +1,248 @@ +//// [tests/cases/compiler/lastTupleElementDestructuring.ts] //// + +=== lastTupleElementDestructuring.ts === +// Test for fixing excess property checking when accessing last tuple element in destructuring +// Fixes https://github.com/microsoft/TypeScript/issues/41548 + +declare function foo(template: T): [T, any, any]; +>foo : (template: T) => [T, any, any] +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^ +>dataType : "a" | "b" +> : ^^^^^^^^^ +>template : T +> : ^ + +declare function bar(template: T): [T, any, any, any]; +>bar : (template: T) => [T, any, any, any] +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^ +>dataType : "a" | "b" +> : ^^^^^^^^^ +>template : T +> : ^ + +// Cases that should NOT error after fix (accessing last element) +const [, , last1] = foo({ dataType: 'a', day: 0 }); +> : undefined +> : ^^^^^^^^^ +> : undefined +> : ^^^^^^^^^ +>last1 : any +> : ^^^ +>foo({ dataType: 'a', day: 0 }) : [{ dataType: "a" | "b"; }, any, any] +> : ^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ +>foo : (template: T) => [T, any, any] +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^ +>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>dataType : "a" +> : ^^^ +>'a' : "a" +> : ^^^ +>day : number +> : ^^^^^^ +>0 : 0 +> : ^ + +const [,,last2] = foo({ dataType: 'a', day: 0 }); +> : undefined +> : ^^^^^^^^^ +> : undefined +> : ^^^^^^^^^ +>last2 : any +> : ^^^ +>foo({ dataType: 'a', day: 0 }) : [{ dataType: "a" | "b"; }, any, any] +> : ^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ +>foo : (template: T) => [T, any, any] +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^ +>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>dataType : "a" +> : ^^^ +>'a' : "a" +> : ^^^ +>day : number +> : ^^^^^^ +>0 : 0 +> : ^ + +const [,,,last3] = bar({ dataType: 'a', day: 0 }); +> : undefined +> : ^^^^^^^^^ +> : undefined +> : ^^^^^^^^^ +> : undefined +> : ^^^^^^^^^ +>last3 : any +> : ^^^ +>bar({ dataType: 'a', day: 0 }) : [{ dataType: "a" | "b"; }, any, any, any] +> : ^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ +>bar : (template: T) => [T, any, any, any] +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^ +>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>dataType : "a" +> : ^^^ +>'a' : "a" +> : ^^^ +>day : number +> : ^^^^^^ +>0 : 0 +> : ^ + +// Cases that already worked (not accessing last element) +const [, mid1, ] = foo({ dataType: 'a', day: 0 }); +> : undefined +> : ^^^^^^^^^ +>mid1 : any +> : ^^^ +>foo({ dataType: 'a', day: 0 }) : [{ dataType: "a"; day: number; }, any, any] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>foo : (template: T) => [T, any, any] +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^ +>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>dataType : "a" +> : ^^^ +>'a' : "a" +> : ^^^ +>day : number +> : ^^^^^^ +>0 : 0 +> : ^ + +const [first1, , ] = foo({ dataType: 'a', day: 0 }); +>first1 : { dataType: "a"; day: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> : undefined +> : ^^^^^^^^^ +>foo({ dataType: 'a', day: 0 }) : [{ dataType: "a"; day: number; }, any, any] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>foo : (template: T) => [T, any, any] +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^ +>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>dataType : "a" +> : ^^^ +>'a' : "a" +> : ^^^ +>day : number +> : ^^^^^^ +>0 : 0 +> : ^ + +const [,,third,] = bar({ dataType: 'a', day: 0 }); +> : undefined +> : ^^^^^^^^^ +> : undefined +> : ^^^^^^^^^ +>third : any +> : ^^^ +>bar({ dataType: 'a', day: 0 }) : [{ dataType: "a"; day: number; }, any, any, any] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>bar : (template: T) => [T, any, any, any] +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^ +>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>dataType : "a" +> : ^^^ +>'a' : "a" +> : ^^^ +>day : number +> : ^^^^^^ +>0 : 0 +> : ^ + +// Legitimate errors should still be caught +const [, , last4] = foo({ dataType: 'c' }); // Error: 'c' not assignable to 'a' | 'b' +> : undefined +> : ^^^^^^^^^ +> : undefined +> : ^^^^^^^^^ +>last4 : any +> : ^^^ +>foo({ dataType: 'c' }) : [{ dataType: "a" | "b"; }, any, any] +> : ^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ +>foo : (template: T) => [T, any, any] +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^ +>{ dataType: 'c' } : { dataType: "c"; } +> : ^^^^^^^^^^^^^^^^^^ +>dataType : "c" +> : ^^^ +>'c' : "c" +> : ^^^ + +const [,,,last5] = bar({ notDataType: 'a' }); // Error: missing required property 'dataType' +> : undefined +> : ^^^^^^^^^ +> : undefined +> : ^^^^^^^^^ +> : undefined +> : ^^^^^^^^^ +>last5 : any +> : ^^^ +>bar({ notDataType: 'a' }) : [{ dataType: "a" | "b"; }, any, any, any] +> : ^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ +>bar : (template: T) => [T, any, any, any] +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^ +>{ notDataType: 'a' } : { notDataType: string; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^ +>notDataType : string +> : ^^^^^^ +>'a' : "a" +> : ^^^ + +// Test with more complex object properties +interface Config { + required: string; +>required : string +> : ^^^^^^ + + optional?: number; +>optional : number +> : ^^^^^^ +} + +declare function withConfig(template: T): [T, string]; +>withConfig : (template: T) => [T, string] +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^ +>template : T +> : ^ + +// Should work - accessing last element with extra property +const [,configStr] = withConfig({ required: 'test', extra: 'should work' }); +> : undefined +> : ^^^^^^^^^ +>configStr : string +> : ^^^^^^ +>withConfig({ required: 'test', extra: 'should work' }) : [{ required: string; extra: string; }, string] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>withConfig : (template: T) => [T, string] +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^ +>{ required: 'test', extra: 'should work' } : { required: string; extra: string; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>required : string +> : ^^^^^^ +>'test' : "test" +> : ^^^^^^ +>extra : string +> : ^^^^^^ +>'should work' : "should work" +> : ^^^^^^^^^^^^^ + +// Should still error - missing required property +const [,configStr2] = withConfig({ optional: 42 }); // Error: missing 'required' +> : undefined +> : ^^^^^^^^^ +>configStr2 : string +> : ^^^^^^ +>withConfig({ optional: 42 }) : [Config, string] +> : ^^^^^^^^^^^^^^^^ +>withConfig : (template: T) => [T, string] +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^ +>{ optional: 42 } : { optional: number; } +> : ^^^^^^^^^^^^^^^^^^^^^ +>optional : number +> : ^^^^^^ +>42 : 42 +> : ^^ + diff --git a/tests/cases/compiler/lastTupleElementDestructuring.ts b/tests/cases/compiler/lastTupleElementDestructuring.ts index 43015dbb039..3a00e59cf5e 100644 --- a/tests/cases/compiler/lastTupleElementDestructuring.ts +++ b/tests/cases/compiler/lastTupleElementDestructuring.ts @@ -1,11 +1,33 @@ // Test for fixing excess property checking when accessing last tuple element in destructuring +// Fixes https://github.com/microsoft/TypeScript/issues/41548 + declare function foo(template: T): [T, any, any]; +declare function bar(template: T): [T, any, any, any]; -// This should NOT error after fix -const [, , last] = foo({ dataType: 'a', day: 0 }); +// Cases that should NOT error after fix (accessing last element) +const [, , last1] = foo({ dataType: 'a', day: 0 }); +const [,,last2] = foo({ dataType: 'a', day: 0 }); +const [,,,last3] = bar({ dataType: 'a', day: 0 }); -// This already works (doesn't access last element) -const [, mid, ] = foo({ dataType: 'a', day: 0 }); +// Cases that already worked (not accessing last element) +const [, mid1, ] = foo({ dataType: 'a', day: 0 }); +const [first1, , ] = foo({ dataType: 'a', day: 0 }); +const [,,third,] = bar({ dataType: 'a', day: 0 }); -// Also test that legitimate errors are still caught -const [, , last2] = foo({ dataType: 'c' }); // Should still error \ No newline at end of file +// Legitimate errors should still be caught +const [, , last4] = foo({ dataType: 'c' }); // Error: 'c' not assignable to 'a' | 'b' +const [,,,last5] = bar({ notDataType: 'a' }); // Error: missing required property 'dataType' + +// Test with more complex object properties +interface Config { + required: string; + optional?: number; +} + +declare function withConfig(template: T): [T, string]; + +// Should work - accessing last element with extra property +const [,configStr] = withConfig({ required: 'test', extra: 'should work' }); + +// Should still error - missing required property +const [,configStr2] = withConfig({ optional: 42 }); // Error: missing 'required' \ No newline at end of file diff --git a/tests/debug_test.ts b/tests/debug_test.ts deleted file mode 100644 index 2a49a4ee07e..00000000000 --- a/tests/debug_test.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Test case to check the fix -declare function foo(template: T): [T, any, any]; - -// Error case - accessing last element -const [, , last] = foo({ dataType: 'a', day: 0 }); - -// Working case - not accessing last element -const [, mid, ] = foo({ dataType: 'a', day: 0 }); \ No newline at end of file