diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 77f35376da7..e255edcdf6f 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -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 { diff --git a/tests/cases/compiler/contextualTypeDestructuringPositionSensitivity.ts b/tests/cases/compiler/contextualTypeDestructuringPositionSensitivity.ts new file mode 100644 index 00000000000..ef7804882d7 --- /dev/null +++ b/tests/cases/compiler/contextualTypeDestructuringPositionSensitivity.ts @@ -0,0 +1,17 @@ +// @strict: true + +type DataType = 'a' | 'b'; +declare function foo(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 \ No newline at end of file