Skip to content
Draft
29 changes: 27 additions & 2 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12334,9 +12334,34 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
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 elementTypes = map(elements, e => isOmittedExpression(e) ? anyType : getTypeFromBindingElement(e, includePatternInType, reportErrors));

// For contextual typing, normalize pattern length to avoid inference differences
// based purely on binding name presence/absence
if (includePatternInType && !restElement) {
// Extend patterns to ensure consistent contextual types across equivalent destructuring operations
const lastBindingIndex = findLastIndex(elements, e => !isOmittedExpression(e), elements.length - 1);
if (lastBindingIndex >= 0) {
// Extend to at least one position beyond the last binding to ensure consistent behavior
// This makes patterns like [, s, ] equivalent to [, s, ,] for contextual typing purposes
const targetLength = lastBindingIndex + 2;
elementTypes = elementTypes.concat(Array(Math.max(0, targetLength - elementTypes.length)).fill(anyType));
}
}

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);
const elementFlags = map(elementTypes, (_, i) => {
if (i < elements.length) {
const e = elements[i];
return e === restElement ? ElementFlags.Rest : i >= minLength ? ElementFlags.Optional : ElementFlags.Required;
}
else {
// Extended elements for contextual typing are optional
return ElementFlags.Optional;
}
});

let result = createTupleType(elementTypes, elementFlags) as TypeReference;
if (includePatternInType) {
result = cloneTypeReference(result);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//// [tests/cases/compiler/contextualTypeArrayBindingPatternConsistency.ts] ////

//// [contextualTypeArrayBindingPatternConsistency.ts]
type DataType = 'a' | 'b';
declare function foo<T extends { dataType: DataType }>(template: T): [T, any, any];

// These should behave identically since they call the same function with the same argument
// but use different destructuring patterns

// Pattern 1: [, , t] - should not have excess property error
const [, , t1] = foo({ dataType: 'a', day: 0 });

// Pattern 2: [, s, ] - should not have excess property error
const [, s1, ] = foo({ dataType: 'a', day: 0 });

// Both patterns should allow the excess property because they produce consistent contextual types
// that don't interfere with generic type inference

// Additional test cases to ensure the fix is general
const [, s2, ] = foo({ dataType: 'b', extra: 'test' }); // [, s, ] pattern with different property
const [, , s3] = foo({ dataType: 'a', another: 1 }); // [, , s] pattern

//// [contextualTypeArrayBindingPatternConsistency.js]
"use strict";
// These should behave identically since they call the same function with the same argument
// but use different destructuring patterns
// Pattern 1: [, , t] - should not have excess property error
var _a = foo({ dataType: 'a', day: 0 }), t1 = _a[2];
// Pattern 2: [, s, ] - should not have excess property error
var _b = foo({ dataType: 'a', day: 0 }), s1 = _b[1];
// Both patterns should allow the excess property because they produce consistent contextual types
// that don't interfere with generic type inference
// Additional test cases to ensure the fix is general
var _c = foo({ dataType: 'b', extra: 'test' }), s2 = _c[1]; // [, s, ] pattern with different property
var _d = foo({ dataType: 'a', another: 1 }), s3 = _d[2]; // [, , s] pattern
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//// [tests/cases/compiler/contextualTypeArrayBindingPatternConsistency.ts] ////

=== contextualTypeArrayBindingPatternConsistency.ts ===
type DataType = 'a' | 'b';
>DataType : Symbol(DataType, Decl(contextualTypeArrayBindingPatternConsistency.ts, 0, 0))

declare function foo<T extends { dataType: DataType }>(template: T): [T, any, any];
>foo : Symbol(foo, Decl(contextualTypeArrayBindingPatternConsistency.ts, 0, 26))
>T : Symbol(T, Decl(contextualTypeArrayBindingPatternConsistency.ts, 1, 21))
>dataType : Symbol(dataType, Decl(contextualTypeArrayBindingPatternConsistency.ts, 1, 32))
>DataType : Symbol(DataType, Decl(contextualTypeArrayBindingPatternConsistency.ts, 0, 0))
>template : Symbol(template, Decl(contextualTypeArrayBindingPatternConsistency.ts, 1, 55))
>T : Symbol(T, Decl(contextualTypeArrayBindingPatternConsistency.ts, 1, 21))
>T : Symbol(T, Decl(contextualTypeArrayBindingPatternConsistency.ts, 1, 21))

// These should behave identically since they call the same function with the same argument
// but use different destructuring patterns

// Pattern 1: [, , t] - should not have excess property error
const [, , t1] = foo({ dataType: 'a', day: 0 });
>t1 : Symbol(t1, Decl(contextualTypeArrayBindingPatternConsistency.ts, 7, 10))
>foo : Symbol(foo, Decl(contextualTypeArrayBindingPatternConsistency.ts, 0, 26))
>dataType : Symbol(dataType, Decl(contextualTypeArrayBindingPatternConsistency.ts, 7, 22))
>day : Symbol(day, Decl(contextualTypeArrayBindingPatternConsistency.ts, 7, 37))

// Pattern 2: [, s, ] - should not have excess property error
const [, s1, ] = foo({ dataType: 'a', day: 0 });
>s1 : Symbol(s1, Decl(contextualTypeArrayBindingPatternConsistency.ts, 10, 8))
>foo : Symbol(foo, Decl(contextualTypeArrayBindingPatternConsistency.ts, 0, 26))
>dataType : Symbol(dataType, Decl(contextualTypeArrayBindingPatternConsistency.ts, 10, 22))
>day : Symbol(day, Decl(contextualTypeArrayBindingPatternConsistency.ts, 10, 37))

// Both patterns should allow the excess property because they produce consistent contextual types
// that don't interfere with generic type inference

// Additional test cases to ensure the fix is general
const [, s2, ] = foo({ dataType: 'b', extra: 'test' }); // [, s, ] pattern with different property
>s2 : Symbol(s2, Decl(contextualTypeArrayBindingPatternConsistency.ts, 16, 8))
>foo : Symbol(foo, Decl(contextualTypeArrayBindingPatternConsistency.ts, 0, 26))
>dataType : Symbol(dataType, Decl(contextualTypeArrayBindingPatternConsistency.ts, 16, 22))
>extra : Symbol(extra, Decl(contextualTypeArrayBindingPatternConsistency.ts, 16, 37))

const [, , s3] = foo({ dataType: 'a', another: 1 }); // [, , s] pattern
>s3 : Symbol(s3, Decl(contextualTypeArrayBindingPatternConsistency.ts, 17, 10))
>foo : Symbol(foo, Decl(contextualTypeArrayBindingPatternConsistency.ts, 0, 26))
>dataType : Symbol(dataType, Decl(contextualTypeArrayBindingPatternConsistency.ts, 17, 22))
>another : Symbol(another, Decl(contextualTypeArrayBindingPatternConsistency.ts, 17, 37))

Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//// [tests/cases/compiler/contextualTypeArrayBindingPatternConsistency.ts] ////

=== contextualTypeArrayBindingPatternConsistency.ts ===
type DataType = 'a' | 'b';
>DataType : DataType
> : ^^^^^^^^

declare function foo<T extends { dataType: DataType }>(template: T): [T, any, any];
>foo : <T extends { dataType: DataType; }>(template: T) => [T, any, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>dataType : DataType
> : ^^^^^^^^
>template : T
> : ^

// These should behave identically since they call the same function with the same argument
// but use different destructuring patterns

// Pattern 1: [, , t] - should not have excess property error
const [, , t1] = foo({ dataType: 'a', day: 0 });
> : undefined
> : ^^^^^^^^^
> : undefined
> : ^^^^^^^^^
>t1 : any
> : ^^^
>foo({ dataType: 'a', day: 0 }) : [{ dataType: "a"; day: number; }, any, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>foo : <T extends { dataType: DataType; }>(template: T) => [T, any, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>dataType : "a"
> : ^^^
>'a' : "a"
> : ^^^
>day : number
> : ^^^^^^
>0 : 0
> : ^

// Pattern 2: [, s, ] - should not have excess property error
const [, s1, ] = foo({ dataType: 'a', day: 0 });
> : undefined
> : ^^^^^^^^^
>s1 : any
> : ^^^
>foo({ dataType: 'a', day: 0 }) : [{ dataType: "a"; day: number; }, any, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>foo : <T extends { dataType: DataType; }>(template: T) => [T, any, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>{ dataType: 'a', day: 0 } : { dataType: "a"; day: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>dataType : "a"
> : ^^^
>'a' : "a"
> : ^^^
>day : number
> : ^^^^^^
>0 : 0
> : ^

// Both patterns should allow the excess property because they produce consistent contextual types
// that don't interfere with generic type inference

// Additional test cases to ensure the fix is general
const [, s2, ] = foo({ dataType: 'b', extra: 'test' }); // [, s, ] pattern with different property
> : undefined
> : ^^^^^^^^^
>s2 : any
> : ^^^
>foo({ dataType: 'b', extra: 'test' }) : [{ dataType: "b"; extra: string; }, any, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>foo : <T extends { dataType: DataType; }>(template: T) => [T, any, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>{ dataType: 'b', extra: 'test' } : { dataType: "b"; extra: string; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>dataType : "b"
> : ^^^
>'b' : "b"
> : ^^^
>extra : string
> : ^^^^^^
>'test' : "test"
> : ^^^^^^

const [, , s3] = foo({ dataType: 'a', another: 1 }); // [, , s] pattern
> : undefined
> : ^^^^^^^^^
> : undefined
> : ^^^^^^^^^
>s3 : any
> : ^^^
>foo({ dataType: 'a', another: 1 }) : [{ dataType: "a"; another: number; }, any, any]
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>foo : <T extends { dataType: DataType; }>(template: T) => [T, any, any]
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
>{ dataType: 'a', another: 1 } : { dataType: "a"; another: number; }
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>dataType : "a"
> : ^^^
>'a' : "a"
> : ^^^
>another : number
> : ^^^^^^
>1 : 1
> : ^

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// @strict: true

type DataType = 'a' | 'b';
declare function foo<T extends { dataType: DataType }>(template: T): [T, any, any];

// These should behave identically since they call the same function with the same argument
// but use different destructuring patterns

// Pattern 1: [, , t] - should not have excess property error
const [, , t1] = foo({ dataType: 'a', day: 0 });

// Pattern 2: [, s, ] - should not have excess property error
const [, s1, ] = foo({ dataType: 'a', day: 0 });

// Both patterns should allow the excess property because they produce consistent contextual types
// that don't interfere with generic type inference

// Additional test cases to ensure the fix is general
const [, s2, ] = foo({ dataType: 'b', extra: 'test' }); // [, s, ] pattern with different property
const [, , s3] = foo({ dataType: 'a', another: 1 }); // [, , s] pattern
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// @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
const [, , t] = foo({ dataType: 'a', day: 0 });
const [, s, ] = foo({ dataType: 'a', day: 0 });