Skip to content
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