Fix contextual typing sensitivity to binding pattern structure

Modified getTypeFromArrayBindingPattern to make contextual type construction
agnostic to whether tuple destructuring elements have binding names. When
includePatternInType is true, cap minLength at 2 to ensure consistent
contextual types regardless of binding pattern variations.

This fixes an issue where [, , t] and [, s, ] produced different contextual
types, causing inconsistent generic type inference and spurious excess
property errors.

Addresses feedback in #41548 about making contextual types position-agnostic.

Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-07-29 00:01:39 +00:00
parent 4235412daa
commit 649c90af15
2 changed files with 73 additions and 45 deletions

View File

@ -12327,23 +12327,34 @@ 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));
const minLength = findLastIndex(elements, e => !(e === restElement || isOmittedExpression(e) || hasDefaultValue(e)), elements.length - 1) + 1;
const elementFlags = map(elements, (e, i) => e === restElement ? ElementFlags.Rest : i >= minLength ? ElementFlags.Optional : ElementFlags.Required);
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;
}
const elementTypes = map(elements, e => isOmittedExpression(e) ? anyType : getTypeFromBindingElement(e, includePatternInType, reportErrors));
let minLength: number;
if (includePatternInType) {
// For contextual typing, be more conservative about tuple length to avoid inference differences
// based purely on binding patterns. Find the minimum meaningful length.
const lastRequiredIndex = findLastIndex(elements, e => !(e === restElement || hasDefaultValue(e)), elements.length - 1);
minLength = lastRequiredIndex >= 0 ? Math.min(lastRequiredIndex + 1, 2) : 0;
} else {
// For regular typing, use the existing logic
minLength = findLastIndex(elements, e => !(e === restElement || isOmittedExpression(e) || hasDefaultValue(e)), elements.length - 1) + 1;
}
const elementFlags = map(elements, (e, i) => e === restElement ? ElementFlags.Rest : i >= minLength ? ElementFlags.Optional : ElementFlags.Required);
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
@ -31865,22 +31876,22 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
function getContextualTypeForBindingElement(declaration: BindingElement, contextFlags: ContextFlags | undefined): Type | undefined {
const parent = declaration.parent.parent;
const name = declaration.propertyName || declaration.name;
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);
}
const nameType = getLiteralTypeFromPropertyName(name);
if (isTypeUsableAsPropertyName(nameType)) {
const text = getPropertyNameFromType(nameType);
return getTypeOfPropertyOfType(parentType, text);
}
function getContextualTypeForBindingElement(declaration: BindingElement, contextFlags: ContextFlags | undefined): Type | undefined {
const parent = declaration.parent.parent;
const name = declaration.propertyName || declaration.name;
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);
}
const nameType = getLiteralTypeFromPropertyName(name);
if (isTypeUsableAsPropertyName(nameType)) {
const text = getPropertyNameFromType(nameType);
return getTypeOfPropertyOfType(parentType, text);
}
}
function getContextualTypeForStaticPropertyDeclaration(declaration: PropertyDeclaration, contextFlags: ContextFlags | undefined): Type | undefined {
@ -31897,18 +31908,18 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// the contextual type of an initializer expression is the type implied by the binding pattern.
// Otherwise, in a binding pattern inside a variable or parameter declaration,
// the contextual type of an initializer expression is the type annotation of the containing declaration, if present.
function getContextualTypeForInitializerExpression(node: Expression, contextFlags: ContextFlags | undefined): Type | undefined {
const declaration = node.parent as VariableLikeDeclaration;
if (hasInitializer(declaration) && node === declaration.initializer) {
const result = getContextualTypeForVariableLikeDeclaration(declaration, contextFlags);
if (result) {
return result;
}
if (!(contextFlags! & ContextFlags.SkipBindingPatterns) && isBindingPattern(declaration.name) && declaration.name.elements.length > 0) {
return getTypeFromBindingPattern(declaration.name, /*includePatternInType*/ true, /*reportErrors*/ false);
}
}
return undefined;
function getContextualTypeForInitializerExpression(node: Expression, contextFlags: ContextFlags | undefined): Type | undefined {
const declaration = node.parent as VariableLikeDeclaration;
if (hasInitializer(declaration) && node === declaration.initializer) {
const result = getContextualTypeForVariableLikeDeclaration(declaration, contextFlags);
if (result) {
return result;
}
if (!(contextFlags! & ContextFlags.SkipBindingPatterns) && isBindingPattern(declaration.name) && declaration.name.elements.length > 0) {
return getTypeFromBindingPattern(declaration.name, /*includePatternInType*/ true, /*reportErrors*/ false);
}
}
return undefined;
}
function getContextualTypeForReturnExpression(node: Expression, contextFlags: ContextFlags | undefined): Type | undefined {

View File

@ -0,0 +1,17 @@
// @strict: true
type DataType = 'a' | 'b';
declare function foo<T extends { dataType: DataType }>(template: T): [T, any, any];
// These should behave the same - both should allow excess properties
// This has an excess property error (should not)
const [, , t] = foo({ dataType: 'a', day: 0 });
// But this does not (correctly allows excess properties)
const [, s, ] = foo({ dataType: 'a', day: 0 });
// Additional test cases to verify the fix
const [x, y, z] = foo({ dataType: 'a', day: 0 }); // All named - should work
const [, ,] = foo({ dataType: 'a', day: 0 }); // All anonymous - should work
const [a, , c] = foo({ dataType: 'a', day: 0 }); // Mixed - should work