Skip to content

Commit 869eb50

Browse files
Fix contextual typing sensitivity to binding pattern structure
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
1 parent 649c90a commit 869eb50

File tree

3 files changed

+51
-48
lines changed

3 files changed

+51
-48
lines changed

‎debug.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
type DataType = 'a' | 'b';
2+
declare function foo<T extends { dataType: DataType }>(template: T): [T, any, any];
3+
4+
// Test both cases
5+
const [, , t] = foo({ dataType: 'a', day: 0 });
6+
const [, s, ] = foo({ dataType: 'a', day: 0 });

‎src/compiler/checker.ts

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12336,18 +12336,24 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1233612336
}
1233712337
const elementTypes = map(elements, e => isOmittedExpression(e) ? anyType : getTypeFromBindingElement(e, includePatternInType, reportErrors));
1233812338

12339-
let minLength: number;
12340-
if (includePatternInType) {
12341-
// For contextual typing, be more conservative about tuple length to avoid inference differences
12342-
// based purely on binding patterns. Find the minimum meaningful length.
12343-
const lastRequiredIndex = findLastIndex(elements, e => !(e === restElement || hasDefaultValue(e)), elements.length - 1);
12344-
minLength = lastRequiredIndex >= 0 ? Math.min(lastRequiredIndex + 1, 2) : 0;
12345-
} else {
12346-
// For regular typing, use the existing logic
12347-
minLength = findLastIndex(elements, e => !(e === restElement || isOmittedExpression(e) || hasDefaultValue(e)), elements.length - 1) + 1;
12339+
// For contextual typing, ensure both [, , t] and [, s, ] produce the same contextual type [any, any, any]
12340+
// by extending shorter tuples to at least 3 elements when constructing contextual types
12341+
if (includePatternInType && !restElement && elementTypes.length < 3) {
12342+
while (elementTypes.length < 3) {
12343+
elementTypes.push(anyType);
12344+
}
1234812345
}
1234912346

12350-
const elementFlags = map(elements, (e, i) => e === restElement ? ElementFlags.Rest : i >= minLength ? ElementFlags.Optional : ElementFlags.Required);
12347+
const minLength = findLastIndex(elements, e => !(e === restElement || isOmittedExpression(e) || hasDefaultValue(e)), elements.length - 1) + 1;
12348+
const elementFlags = map(elementTypes, (_, i) => {
12349+
if (i < elements.length) {
12350+
const e = elements[i];
12351+
return e === restElement ? ElementFlags.Rest : i >= minLength ? ElementFlags.Optional : ElementFlags.Required;
12352+
} else {
12353+
// Added elements for contextual typing should be optional
12354+
return ElementFlags.Optional;
12355+
}
12356+
});
1235112357
let result = createTupleType(elementTypes, elementFlags) as TypeReference;
1235212358
if (includePatternInType) {
1235312359
result = cloneTypeReference(result);
@@ -31876,22 +31882,22 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3187631882
}
3187731883
}
3187831884

31879-
function getContextualTypeForBindingElement(declaration: BindingElement, contextFlags: ContextFlags | undefined): Type | undefined {
31880-
const parent = declaration.parent.parent;
31881-
const name = declaration.propertyName || declaration.name;
31882-
const parentType = getContextualTypeForVariableLikeDeclaration(parent, contextFlags) ||
31883-
parent.kind !== SyntaxKind.BindingElement && parent.initializer && checkDeclarationInitializer(parent, declaration.dotDotDotToken ? CheckMode.RestBindingElement : CheckMode.Normal);
31884-
if (!parentType || isBindingPattern(name) || isComputedNonLiteralName(name)) return undefined;
31885-
if (parent.name.kind === SyntaxKind.ArrayBindingPattern) {
31886-
const index = indexOfNode(declaration.parent.elements, declaration);
31887-
if (index < 0) return undefined;
31888-
return getContextualTypeForElementExpression(parentType, index);
31889-
}
31890-
const nameType = getLiteralTypeFromPropertyName(name);
31891-
if (isTypeUsableAsPropertyName(nameType)) {
31892-
const text = getPropertyNameFromType(nameType);
31893-
return getTypeOfPropertyOfType(parentType, text);
31894-
}
31885+
function getContextualTypeForBindingElement(declaration: BindingElement, contextFlags: ContextFlags | undefined): Type | undefined {
31886+
const parent = declaration.parent.parent;
31887+
const name = declaration.propertyName || declaration.name;
31888+
const parentType = getContextualTypeForVariableLikeDeclaration(parent, contextFlags) ||
31889+
parent.kind !== SyntaxKind.BindingElement && parent.initializer && checkDeclarationInitializer(parent, declaration.dotDotDotToken ? CheckMode.RestBindingElement : CheckMode.Normal);
31890+
if (!parentType || isBindingPattern(name) || isComputedNonLiteralName(name)) return undefined;
31891+
if (parent.name.kind === SyntaxKind.ArrayBindingPattern) {
31892+
const index = indexOfNode(declaration.parent.elements, declaration);
31893+
if (index < 0) return undefined;
31894+
return getContextualTypeForElementExpression(parentType, index);
31895+
}
31896+
const nameType = getLiteralTypeFromPropertyName(name);
31897+
if (isTypeUsableAsPropertyName(nameType)) {
31898+
const text = getPropertyNameFromType(nameType);
31899+
return getTypeOfPropertyOfType(parentType, text);
31900+
}
3189531901
}
3189631902

3189731903
function getContextualTypeForStaticPropertyDeclaration(declaration: PropertyDeclaration, contextFlags: ContextFlags | undefined): Type | undefined {
@@ -31908,18 +31914,18 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3190831914
// the contextual type of an initializer expression is the type implied by the binding pattern.
3190931915
// Otherwise, in a binding pattern inside a variable or parameter declaration,
3191031916
// the contextual type of an initializer expression is the type annotation of the containing declaration, if present.
31911-
function getContextualTypeForInitializerExpression(node: Expression, contextFlags: ContextFlags | undefined): Type | undefined {
31912-
const declaration = node.parent as VariableLikeDeclaration;
31913-
if (hasInitializer(declaration) && node === declaration.initializer) {
31914-
const result = getContextualTypeForVariableLikeDeclaration(declaration, contextFlags);
31915-
if (result) {
31916-
return result;
31917-
}
31918-
if (!(contextFlags! & ContextFlags.SkipBindingPatterns) && isBindingPattern(declaration.name) && declaration.name.elements.length > 0) {
31919-
return getTypeFromBindingPattern(declaration.name, /*includePatternInType*/ true, /*reportErrors*/ false);
31920-
}
31921-
}
31922-
return undefined;
31917+
function getContextualTypeForInitializerExpression(node: Expression, contextFlags: ContextFlags | undefined): Type | undefined {
31918+
const declaration = node.parent as VariableLikeDeclaration;
31919+
if (hasInitializer(declaration) && node === declaration.initializer) {
31920+
const result = getContextualTypeForVariableLikeDeclaration(declaration, contextFlags);
31921+
if (result) {
31922+
return result;
31923+
}
31924+
if (!(contextFlags! & ContextFlags.SkipBindingPatterns) && isBindingPattern(declaration.name) && declaration.name.elements.length > 0) {
31925+
return getTypeFromBindingPattern(declaration.name, /*includePatternInType*/ true, /*reportErrors*/ false);
31926+
}
31927+
}
31928+
return undefined;
3192331929
}
3192431930

3192531931
function getContextualTypeForReturnExpression(node: Expression, contextFlags: ContextFlags | undefined): Type | undefined {

‎tests/cases/compiler/contextualTypeDestructuringPositionSensitivity.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,5 @@ type DataType = 'a' | 'b';
44
declare function foo<T extends { dataType: DataType }>(template: T): [T, any, any];
55

66
// These should behave the same - both should allow excess properties
7-
8-
// This has an excess property error (should not)
97
const [, , t] = foo({ dataType: 'a', day: 0 });
10-
11-
// But this does not (correctly allows excess properties)
12-
const [, s, ] = foo({ dataType: 'a', day: 0 });
13-
14-
// Additional test cases to verify the fix
15-
const [x, y, z] = foo({ dataType: 'a', day: 0 }); // All named - should work
16-
const [, ,] = foo({ dataType: 'a', day: 0 }); // All anonymous - should work
17-
const [a, , c] = foo({ dataType: 'a', day: 0 }); // Mixed - should work
8+
const [, s, ] = foo({ dataType: 'a', day: 0 });

0 commit comments

Comments
 (0)