Skip to content

Fix contextual typing sensitivity to binding pattern structure in destructuring #62142

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

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
101 changes: 56 additions & 45 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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