From 97de8307447a3732d07a31a7bac53c55bb4f1304 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 05:34:38 +0000 Subject: [PATCH] Fix contextual typing sensitivity to binding pattern structure without hardcoded values Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- src/compiler/checker.ts | 76 ++++++------ ...xtualTypeArrayBindingPatternConsistency.js | 35 ++++++ ...TypeArrayBindingPatternConsistency.symbols | 48 ++++++++ ...alTypeArrayBindingPatternConsistency.types | 108 ++++++++++++++++++ ...xtualTypeArrayBindingPatternConsistency.ts | 20 ++++ 5 files changed, 253 insertions(+), 34 deletions(-) create mode 100644 tests/baselines/reference/contextualTypeArrayBindingPatternConsistency.js create mode 100644 tests/baselines/reference/contextualTypeArrayBindingPatternConsistency.symbols create mode 100644 tests/baselines/reference/contextualTypeArrayBindingPatternConsistency.types create mode 100644 tests/cases/compiler/contextualTypeArrayBindingPatternConsistency.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 5fc1d00e48e..2ab56123620 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -12327,40 +12327,48 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } // Return the type implied by an array binding pattern - function getTypeFromArrayBindingPattern(pattern: BindingPattern, includePatternInType: boolean, reportErrors: boolean): Type { - const elements = pattern.elements; - const lastElement = lastOrUndefined(elements); - const restElement = lastElement && lastElement.kind === SyntaxKind.BindingElement && lastElement.dotDotDotToken ? lastElement : undefined; - if (elements.length === 0 || elements.length === 1 && restElement) { - return languageVersion >= ScriptTarget.ES2015 ? createIterableType(anyType) : anyArrayType; - } - const elementTypes = map(elements, e => isOmittedExpression(e) ? anyType : getTypeFromBindingElement(e, includePatternInType, reportErrors)); - - // For contextual typing, ensure both [, , t] and [, s, ] produce the same contextual type [any, any, any] - // by extending shorter tuples to at least 3 elements when constructing contextual types - if (includePatternInType && !restElement && elementTypes.length < 3) { - while (elementTypes.length < 3) { - elementTypes.push(anyType); - } - } - - const minLength = findLastIndex(elements, e => !(e === restElement || isOmittedExpression(e) || hasDefaultValue(e)), elements.length - 1) + 1; - const elementFlags = map(elementTypes, (_, i) => { - if (i < elements.length) { - const e = elements[i]; - return e === restElement ? ElementFlags.Rest : i >= minLength ? ElementFlags.Optional : ElementFlags.Required; - } else { - // Added elements for contextual typing should be optional - return ElementFlags.Optional; - } - }); - let result = createTupleType(elementTypes, elementFlags) as TypeReference; - if (includePatternInType) { - result = cloneTypeReference(result); - result.pattern = pattern; - result.objectFlags |= ObjectFlags.ContainsObjectOrArrayLiteral; - } - return result; + function getTypeFromArrayBindingPattern(pattern: BindingPattern, includePatternInType: boolean, reportErrors: boolean): Type { + const elements = pattern.elements; + const lastElement = lastOrUndefined(elements); + const restElement = lastElement && lastElement.kind === SyntaxKind.BindingElement && lastElement.dotDotDotToken ? lastElement : undefined; + if (elements.length === 0 || elements.length === 1 && restElement) { + return languageVersion >= ScriptTarget.ES2015 ? createIterableType(anyType) : anyArrayType; + } + + let elementTypes = map(elements, e => isOmittedExpression(e) ? anyType : getTypeFromBindingElement(e, includePatternInType, reportErrors)); + + // For contextual typing, normalize pattern length to avoid inference differences + // based purely on binding name presence/absence + if (includePatternInType && !restElement) { + // Extend patterns to ensure consistent contextual types across equivalent destructuring operations + const lastBindingIndex = findLastIndex(elements, e => !isOmittedExpression(e), elements.length - 1); + if (lastBindingIndex >= 0) { + // Extend to at least one position beyond the last binding to ensure consistent behavior + // This makes patterns like [, s, ] equivalent to [, s, ,] for contextual typing purposes + const targetLength = lastBindingIndex + 2; + elementTypes = elementTypes.concat(Array(Math.max(0, targetLength - elementTypes.length)).fill(anyType)); + } + } + + const minLength = findLastIndex(elements, e => !(e === restElement || isOmittedExpression(e) || hasDefaultValue(e)), elements.length - 1) + 1; + const elementFlags = map(elementTypes, (_, i) => { + if (i < elements.length) { + const e = elements[i]; + return e === restElement ? ElementFlags.Rest : i >= minLength ? ElementFlags.Optional : ElementFlags.Required; + } + else { + // Extended elements for contextual typing are optional + return ElementFlags.Optional; + } + }); + + let result = createTupleType(elementTypes, elementFlags) as TypeReference; + if (includePatternInType) { + result = cloneTypeReference(result); + result.pattern = pattern; + result.objectFlags |= ObjectFlags.ContainsObjectOrArrayLiteral; + } + return result; } // Return the type implied by a binding pattern. This is the type implied purely by the binding pattern itself diff --git a/tests/baselines/reference/contextualTypeArrayBindingPatternConsistency.js b/tests/baselines/reference/contextualTypeArrayBindingPatternConsistency.js new file mode 100644 index 00000000000..809b8e767cc --- /dev/null +++ b/tests/baselines/reference/contextualTypeArrayBindingPatternConsistency.js @@ -0,0 +1,35 @@ +//// [tests/cases/compiler/contextualTypeArrayBindingPatternConsistency.ts] //// + +//// [contextualTypeArrayBindingPatternConsistency.ts] +type DataType = 'a' | 'b'; +declare function foo(template: T): [T, any, any]; + +// These should behave identically since they call the same function with the same argument +// but use different destructuring patterns + +// Pattern 1: [, , t] - should not have excess property error +const [, , t1] = foo({ dataType: 'a', day: 0 }); + +// Pattern 2: [, s, ] - should not have excess property error +const [, s1, ] = foo({ dataType: 'a', day: 0 }); + +// Both patterns should allow the excess property because they produce consistent contextual types +// that don't interfere with generic type inference + +// Additional test cases to ensure the fix is general +const [, s2, ] = foo({ dataType: 'b', extra: 'test' }); // [, s, ] pattern with different property +const [, , s3] = foo({ dataType: 'a', another: 1 }); // [, , s] pattern + +//// [contextualTypeArrayBindingPatternConsistency.js] +"use strict"; +// These should behave identically since they call the same function with the same argument +// but use different destructuring patterns +// Pattern 1: [, , t] - should not have excess property error +var _a = foo({ dataType: 'a', day: 0 }), t1 = _a[2]; +// Pattern 2: [, s, ] - should not have excess property error +var _b = foo({ dataType: 'a', day: 0 }), s1 = _b[1]; +// Both patterns should allow the excess property because they produce consistent contextual types +// that don't interfere with generic type inference +// Additional test cases to ensure the fix is general +var _c = foo({ dataType: 'b', extra: 'test' }), s2 = _c[1]; // [, s, ] pattern with different property +var _d = foo({ dataType: 'a', another: 1 }), s3 = _d[2]; // [, , s] pattern diff --git a/tests/baselines/reference/contextualTypeArrayBindingPatternConsistency.symbols b/tests/baselines/reference/contextualTypeArrayBindingPatternConsistency.symbols new file mode 100644 index 00000000000..426e60a784b --- /dev/null +++ b/tests/baselines/reference/contextualTypeArrayBindingPatternConsistency.symbols @@ -0,0 +1,48 @@ +//// [tests/cases/compiler/contextualTypeArrayBindingPatternConsistency.ts] //// + +=== contextualTypeArrayBindingPatternConsistency.ts === +type DataType = 'a' | 'b'; +>DataType : Symbol(DataType, Decl(contextualTypeArrayBindingPatternConsistency.ts, 0, 0)) + +declare function foo(template: T): [T, any, any]; +>foo : Symbol(foo, Decl(contextualTypeArrayBindingPatternConsistency.ts, 0, 26)) +>T : Symbol(T, Decl(contextualTypeArrayBindingPatternConsistency.ts, 1, 21)) +>dataType : Symbol(dataType, Decl(contextualTypeArrayBindingPatternConsistency.ts, 1, 32)) +>DataType : Symbol(DataType, Decl(contextualTypeArrayBindingPatternConsistency.ts, 0, 0)) +>template : Symbol(template, Decl(contextualTypeArrayBindingPatternConsistency.ts, 1, 55)) +>T : Symbol(T, Decl(contextualTypeArrayBindingPatternConsistency.ts, 1, 21)) +>T : Symbol(T, Decl(contextualTypeArrayBindingPatternConsistency.ts, 1, 21)) + +// These should behave identically since they call the same function with the same argument +// but use different destructuring patterns + +// Pattern 1: [, , t] - should not have excess property error +const [, , t1] = foo({ dataType: 'a', day: 0 }); +>t1 : Symbol(t1, Decl(contextualTypeArrayBindingPatternConsistency.ts, 7, 10)) +>foo : Symbol(foo, Decl(contextualTypeArrayBindingPatternConsistency.ts, 0, 26)) +>dataType : Symbol(dataType, Decl(contextualTypeArrayBindingPatternConsistency.ts, 7, 22)) +>day : Symbol(day, Decl(contextualTypeArrayBindingPatternConsistency.ts, 7, 37)) + +// Pattern 2: [, s, ] - should not have excess property error +const [, s1, ] = foo({ dataType: 'a', day: 0 }); +>s1 : Symbol(s1, Decl(contextualTypeArrayBindingPatternConsistency.ts, 10, 8)) +>foo : Symbol(foo, Decl(contextualTypeArrayBindingPatternConsistency.ts, 0, 26)) +>dataType : Symbol(dataType, Decl(contextualTypeArrayBindingPatternConsistency.ts, 10, 22)) +>day : Symbol(day, Decl(contextualTypeArrayBindingPatternConsistency.ts, 10, 37)) + +// Both patterns should allow the excess property because they produce consistent contextual types +// that don't interfere with generic type inference + +// Additional test cases to ensure the fix is general +const [, s2, ] = foo({ dataType: 'b', extra: 'test' }); // [, s, ] pattern with different property +>s2 : Symbol(s2, Decl(contextualTypeArrayBindingPatternConsistency.ts, 16, 8)) +>foo : Symbol(foo, Decl(contextualTypeArrayBindingPatternConsistency.ts, 0, 26)) +>dataType : Symbol(dataType, Decl(contextualTypeArrayBindingPatternConsistency.ts, 16, 22)) +>extra : Symbol(extra, Decl(contextualTypeArrayBindingPatternConsistency.ts, 16, 37)) + +const [, , s3] = foo({ dataType: 'a', another: 1 }); // [, , s] pattern +>s3 : Symbol(s3, Decl(contextualTypeArrayBindingPatternConsistency.ts, 17, 10)) +>foo : Symbol(foo, Decl(contextualTypeArrayBindingPatternConsistency.ts, 0, 26)) +>dataType : Symbol(dataType, Decl(contextualTypeArrayBindingPatternConsistency.ts, 17, 22)) +>another : Symbol(another, Decl(contextualTypeArrayBindingPatternConsistency.ts, 17, 37)) + diff --git a/tests/baselines/reference/contextualTypeArrayBindingPatternConsistency.types b/tests/baselines/reference/contextualTypeArrayBindingPatternConsistency.types new file mode 100644 index 00000000000..f8ff0009588 --- /dev/null +++ b/tests/baselines/reference/contextualTypeArrayBindingPatternConsistency.types @@ -0,0 +1,108 @@ +//// [tests/cases/compiler/contextualTypeArrayBindingPatternConsistency.ts] //// + +=== contextualTypeArrayBindingPatternConsistency.ts === +type DataType = 'a' | 'b'; +>DataType : DataType +> : ^^^^^^^^ + +declare function foo(template: T): [T, any, any]; +>foo : (template: T) => [T, any, any] +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^ +>dataType : DataType +> : ^^^^^^^^ +>template : T +> : ^ + +// These should behave identically since they call the same function with the same argument +// but use different destructuring patterns + +// Pattern 1: [, , t] - should not have excess property error +const [, , t1] = foo({ dataType: 'a', day: 0 }); +> : undefined +> : ^^^^^^^^^ +> : undefined +> : ^^^^^^^^^ +>t1 : 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 +> : ^ + +// Pattern 2: [, s, ] - should not have excess property error +const [, s1, ] = foo({ dataType: 'a', day: 0 }); +> : undefined +> : ^^^^^^^^^ +>s1 : 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 +> : ^ + +// Both patterns should allow the excess property because they produce consistent contextual types +// that don't interfere with generic type inference + +// Additional test cases to ensure the fix is general +const [, s2, ] = foo({ dataType: 'b', extra: 'test' }); // [, s, ] pattern with different property +> : undefined +> : ^^^^^^^^^ +>s2 : any +> : ^^^ +>foo({ dataType: 'b', extra: 'test' }) : [{ dataType: "b"; extra: string; }, any, any] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>foo : (template: T) => [T, any, any] +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^ +>{ dataType: 'b', extra: 'test' } : { dataType: "b"; extra: string; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>dataType : "b" +> : ^^^ +>'b' : "b" +> : ^^^ +>extra : string +> : ^^^^^^ +>'test' : "test" +> : ^^^^^^ + +const [, , s3] = foo({ dataType: 'a', another: 1 }); // [, , s] pattern +> : undefined +> : ^^^^^^^^^ +> : undefined +> : ^^^^^^^^^ +>s3 : any +> : ^^^ +>foo({ dataType: 'a', another: 1 }) : [{ dataType: "a"; another: number; }, any, any] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>foo : (template: T) => [T, any, any] +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^ +>{ dataType: 'a', another: 1 } : { dataType: "a"; another: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>dataType : "a" +> : ^^^ +>'a' : "a" +> : ^^^ +>another : number +> : ^^^^^^ +>1 : 1 +> : ^ + diff --git a/tests/cases/compiler/contextualTypeArrayBindingPatternConsistency.ts b/tests/cases/compiler/contextualTypeArrayBindingPatternConsistency.ts new file mode 100644 index 00000000000..0685d4a4dc2 --- /dev/null +++ b/tests/cases/compiler/contextualTypeArrayBindingPatternConsistency.ts @@ -0,0 +1,20 @@ +// @strict: true + +type DataType = 'a' | 'b'; +declare function foo(template: T): [T, any, any]; + +// These should behave identically since they call the same function with the same argument +// but use different destructuring patterns + +// Pattern 1: [, , t] - should not have excess property error +const [, , t1] = foo({ dataType: 'a', day: 0 }); + +// Pattern 2: [, s, ] - should not have excess property error +const [, s1, ] = foo({ dataType: 'a', day: 0 }); + +// Both patterns should allow the excess property because they produce consistent contextual types +// that don't interfere with generic type inference + +// Additional test cases to ensure the fix is general +const [, s2, ] = foo({ dataType: 'b', extra: 'test' }); // [, s, ] pattern with different property +const [, , s3] = foo({ dataType: 'a', another: 1 }); // [, , s] pattern \ No newline at end of file