Skip to content

Expand the tuple contextual type to the target's shape when it comes from a binding pattern #52630

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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16208,6 +16208,46 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
/*readonly*/ false, target.labeledElementDeclarations && target.labeledElementDeclarations.slice(index, endIndex));
}

function expandTupleShape(source: TupleTypeReference, target: TupleTypeReference): Type {
const sourceArity = getTypeReferenceArity(source);
const targetArity = getTypeReferenceArity(target);
const sourceElementTypes = getTypeArguments(source);
const targetElementTypes = getTypeArguments(target);

let resultTypes: Type[] | undefined;
let resultFlags: ElementFlags[] | undefined;

for (let i = 0; i < targetArity; i++) {
if (i >= sourceArity) {
if (!resultTypes) {
resultTypes = sourceElementTypes.slice();
resultFlags = source.target.elementFlags.slice();
}
const targetFlag = target.target.elementFlags[i];
resultTypes.push(anyType);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than any with the target flags, shouldn't it be never with the optional flag? To reflect how the consumer never reads the member?

resultFlags!.push(targetFlag);
}
else if (isTupleType(sourceElementTypes[i]) && isTupleType(targetElementTypes[i])) {
const result = expandTupleShape(sourceElementTypes[i] as TupleTypeReference, targetElementTypes[i] as TupleTypeReference);
if (result === sourceElementTypes[i]) {
continue;
}
if (!resultTypes) {
resultTypes = sourceElementTypes.slice(0, i);
resultFlags = source.target.elementFlags.slice(0, i);
}
resultTypes.push(result);
resultFlags!.push(source.target.elementFlags[i]);
}
else if (resultTypes) {
resultTypes.push(sourceElementTypes[i]);
resultFlags!.push(source.target.elementFlags[i]);
}
}

return resultTypes ? createTupleType(resultTypes, resultFlags) : source;
}

function getKnownKeysOfTupleType(type: TupleTypeReference) {
return getUnionType(append(arrayOf(type.target.fixedLength, i => getStringLiteralType("" + i)),
getIndexType(type.target.readonly ? globalReadonlyArrayType : globalArrayType)));
Expand Down Expand Up @@ -32443,7 +32483,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// return type of 'wrap'.
if (node.kind !== SyntaxKind.Decorator) {
const skipBindingPatterns = every(signature.typeParameters, p => !!getDefaultFromTypeParameter(p));
const contextualType = getContextualType(node, skipBindingPatterns ? ContextFlags.SkipBindingPatterns : ContextFlags.None);
let contextualType = getContextualType(node, skipBindingPatterns ? ContextFlags.SkipBindingPatterns : ContextFlags.None);
if (contextualType) {
const inferenceTargetType = getReturnTypeOfSignature(signature);
if (couldContainTypeVariables(inferenceTargetType)) {
Expand Down Expand Up @@ -32479,6 +32519,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// Inferences made from return types have lower priority than all other inferences.
inferTypes(context.inferences, inferenceSourceType, inferenceTargetType, InferencePriority.ReturnType);
}
// if the contextual type is a tuple and it's is coming from a binding pattern
// then we might need to expand it to match the arity of the target type
// this allows us to infer tuples even if the binding pattern is shorter than the target type, like here:
// declare function fn<T>(t: T) => [T, string[]];
// const [[x]] = fn(['hi']);
else if (isTupleType(contextualType) && isTupleType(inferenceTargetType)) {
contextualType = expandTupleShape(contextualType, inferenceTargetType);
}
// Create a type mapper for instantiating generic contextual types using the inferences made
// from the return type. We need a separate inference pass here because (a) instantiation of
// the source type uses the outer context's return mapper (which excludes inferences made from
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//// [tests/cases/compiler/inferTupleFromBindingPattern.ts] ////

//// [inferTupleFromBindingPattern.ts]
declare function f<T>(cb: () => T): T;
const [e1, e2, e3] = f(() => [1, "hi", true]);

// repro from #42969
declare const f2: <T extends string[]>(t: T) => [T, string[]];
const [[f2e1]] = f2(['1']);
f2e1.toLowerCase();

declare const f3: <T extends string[]>(t: T) => [[T, string[]]];
const [[[f3e1]]] = f3(['1']);
f3e1.toLowerCase();

//// [inferTupleFromBindingPattern.js]
"use strict";
var _a = f(function () { return [1, "hi", true]; }), e1 = _a[0], e2 = _a[1], e3 = _a[2];
var f2e1 = f2(['1'])[0][0];
f2e1.toLowerCase();
var f3e1 = f3(['1'])[0][0][0];
f3e1.toLowerCase();
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//// [tests/cases/compiler/inferTupleFromBindingPattern.ts] ////

=== inferTupleFromBindingPattern.ts ===
declare function f<T>(cb: () => T): T;
>f : Symbol(f, Decl(inferTupleFromBindingPattern.ts, 0, 0))
>T : Symbol(T, Decl(inferTupleFromBindingPattern.ts, 0, 19))
>cb : Symbol(cb, Decl(inferTupleFromBindingPattern.ts, 0, 22))
>T : Symbol(T, Decl(inferTupleFromBindingPattern.ts, 0, 19))
>T : Symbol(T, Decl(inferTupleFromBindingPattern.ts, 0, 19))

const [e1, e2, e3] = f(() => [1, "hi", true]);
>e1 : Symbol(e1, Decl(inferTupleFromBindingPattern.ts, 1, 7))
>e2 : Symbol(e2, Decl(inferTupleFromBindingPattern.ts, 1, 10))
>e3 : Symbol(e3, Decl(inferTupleFromBindingPattern.ts, 1, 14))
>f : Symbol(f, Decl(inferTupleFromBindingPattern.ts, 0, 0))

// repro from #42969
declare const f2: <T extends string[]>(t: T) => [T, string[]];
>f2 : Symbol(f2, Decl(inferTupleFromBindingPattern.ts, 4, 13))
>T : Symbol(T, Decl(inferTupleFromBindingPattern.ts, 4, 19))
>t : Symbol(t, Decl(inferTupleFromBindingPattern.ts, 4, 39))
>T : Symbol(T, Decl(inferTupleFromBindingPattern.ts, 4, 19))
>T : Symbol(T, Decl(inferTupleFromBindingPattern.ts, 4, 19))

const [[f2e1]] = f2(['1']);
>f2e1 : Symbol(f2e1, Decl(inferTupleFromBindingPattern.ts, 5, 8))
>f2 : Symbol(f2, Decl(inferTupleFromBindingPattern.ts, 4, 13))

f2e1.toLowerCase();
>f2e1.toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --))
>f2e1 : Symbol(f2e1, Decl(inferTupleFromBindingPattern.ts, 5, 8))
>toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --))

declare const f3: <T extends string[]>(t: T) => [[T, string[]]];
>f3 : Symbol(f3, Decl(inferTupleFromBindingPattern.ts, 8, 13))
>T : Symbol(T, Decl(inferTupleFromBindingPattern.ts, 8, 19))
>t : Symbol(t, Decl(inferTupleFromBindingPattern.ts, 8, 39))
>T : Symbol(T, Decl(inferTupleFromBindingPattern.ts, 8, 19))
>T : Symbol(T, Decl(inferTupleFromBindingPattern.ts, 8, 19))

const [[[f3e1]]] = f3(['1']);
>f3e1 : Symbol(f3e1, Decl(inferTupleFromBindingPattern.ts, 9, 9))
>f3 : Symbol(f3, Decl(inferTupleFromBindingPattern.ts, 8, 13))

f3e1.toLowerCase();
>f3e1.toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --))
>f3e1 : Symbol(f3e1, Decl(inferTupleFromBindingPattern.ts, 9, 9))
>toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --))

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

=== inferTupleFromBindingPattern.ts ===
declare function f<T>(cb: () => T): T;
>f : <T>(cb: () => T) => T
>cb : () => T

const [e1, e2, e3] = f(() => [1, "hi", true]);
>e1 : number
>e2 : string
>e3 : boolean
>f(() => [1, "hi", true]) : [number, string, boolean]
>f : <T>(cb: () => T) => T
>() => [1, "hi", true] : () => [number, string, boolean]
>[1, "hi", true] : [number, string, true]
>1 : 1
>"hi" : "hi"
>true : true

// repro from #42969
declare const f2: <T extends string[]>(t: T) => [T, string[]];
>f2 : <T extends string[]>(t: T) => [T, string[]]
>t : T

const [[f2e1]] = f2(['1']);
>f2e1 : string
>f2(['1']) : [[string], string[]]
>f2 : <T extends string[]>(t: T) => [T, string[]]
>['1'] : [string]
>'1' : "1"

f2e1.toLowerCase();
>f2e1.toLowerCase() : string
>f2e1.toLowerCase : () => string
>f2e1 : string
>toLowerCase : () => string

declare const f3: <T extends string[]>(t: T) => [[T, string[]]];
>f3 : <T extends string[]>(t: T) => [[T, string[]]]
>t : T

const [[[f3e1]]] = f3(['1']);
>f3e1 : string
>f3(['1']) : [[[string], string[]]]
>f3 : <T extends string[]>(t: T) => [[T, string[]]]
>['1'] : [string]
>'1' : "1"

f3e1.toLowerCase();
>f3e1.toLowerCase() : string
>f3e1.toLowerCase : () => string
>f3e1 : string
>toLowerCase : () => string

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

//// [inferTupleFromBindingPattern.ts]
declare function f<T>(cb: () => T): T;
const [e1, e2, e3] = f(() => [1, "hi", true]);

// repro from #42969
declare const f2: <T extends string[]>(t: T) => [T, string[]];
const [[f2e1]] = f2(['1']);
f2e1.toLowerCase();

declare const f3: <T extends string[]>(t: T) => [[T, string[]]];
const [[[f3e1]]] = f3(['1']);
f3e1.toLowerCase();

//// [inferTupleFromBindingPattern.js]
"use strict";
var _a = f(function () { return [1, "hi", true]; }), e1 = _a[0], e2 = _a[1], e3 = _a[2];
var f2e1 = f2(['1'])[0][0];
f2e1.toLowerCase();
var f3e1 = f3(['1'])[0][0][0];
f3e1.toLowerCase();
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//// [tests/cases/compiler/inferTupleFromBindingPattern.ts] ////

=== inferTupleFromBindingPattern.ts ===
declare function f<T>(cb: () => T): T;
>f : Symbol(f, Decl(inferTupleFromBindingPattern.ts, 0, 0))
>T : Symbol(T, Decl(inferTupleFromBindingPattern.ts, 0, 19))
>cb : Symbol(cb, Decl(inferTupleFromBindingPattern.ts, 0, 22))
>T : Symbol(T, Decl(inferTupleFromBindingPattern.ts, 0, 19))
>T : Symbol(T, Decl(inferTupleFromBindingPattern.ts, 0, 19))

const [e1, e2, e3] = f(() => [1, "hi", true]);
>e1 : Symbol(e1, Decl(inferTupleFromBindingPattern.ts, 1, 7))
>e2 : Symbol(e2, Decl(inferTupleFromBindingPattern.ts, 1, 10))
>e3 : Symbol(e3, Decl(inferTupleFromBindingPattern.ts, 1, 14))
>f : Symbol(f, Decl(inferTupleFromBindingPattern.ts, 0, 0))

// repro from #42969
declare const f2: <T extends string[]>(t: T) => [T, string[]];
>f2 : Symbol(f2, Decl(inferTupleFromBindingPattern.ts, 4, 13))
>T : Symbol(T, Decl(inferTupleFromBindingPattern.ts, 4, 19))
>t : Symbol(t, Decl(inferTupleFromBindingPattern.ts, 4, 39))
>T : Symbol(T, Decl(inferTupleFromBindingPattern.ts, 4, 19))
>T : Symbol(T, Decl(inferTupleFromBindingPattern.ts, 4, 19))

const [[f2e1]] = f2(['1']);
>f2e1 : Symbol(f2e1, Decl(inferTupleFromBindingPattern.ts, 5, 8))
>f2 : Symbol(f2, Decl(inferTupleFromBindingPattern.ts, 4, 13))

f2e1.toLowerCase();
>f2e1.toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --))
>f2e1 : Symbol(f2e1, Decl(inferTupleFromBindingPattern.ts, 5, 8))
>toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --))

declare const f3: <T extends string[]>(t: T) => [[T, string[]]];
>f3 : Symbol(f3, Decl(inferTupleFromBindingPattern.ts, 8, 13))
>T : Symbol(T, Decl(inferTupleFromBindingPattern.ts, 8, 19))
>t : Symbol(t, Decl(inferTupleFromBindingPattern.ts, 8, 39))
>T : Symbol(T, Decl(inferTupleFromBindingPattern.ts, 8, 19))
>T : Symbol(T, Decl(inferTupleFromBindingPattern.ts, 8, 19))

const [[[f3e1]]] = f3(['1']);
>f3e1 : Symbol(f3e1, Decl(inferTupleFromBindingPattern.ts, 9, 9))
>f3 : Symbol(f3, Decl(inferTupleFromBindingPattern.ts, 8, 13))

f3e1.toLowerCase();
>f3e1.toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --))
>f3e1 : Symbol(f3e1, Decl(inferTupleFromBindingPattern.ts, 9, 9))
>toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --))

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

=== inferTupleFromBindingPattern.ts ===
declare function f<T>(cb: () => T): T;
>f : <T>(cb: () => T) => T
>cb : () => T

const [e1, e2, e3] = f(() => [1, "hi", true]);
>e1 : number
>e2 : string
>e3 : boolean
>f(() => [1, "hi", true]) : [number, string, boolean]
>f : <T>(cb: () => T) => T
>() => [1, "hi", true] : () => [number, string, boolean]
>[1, "hi", true] : [number, string, true]
>1 : 1
>"hi" : "hi"
>true : true

// repro from #42969
declare const f2: <T extends string[]>(t: T) => [T, string[]];
>f2 : <T extends string[]>(t: T) => [T, string[]]
>t : T

const [[f2e1]] = f2(['1']);
>f2e1 : string
>f2(['1']) : [[string], string[]]
>f2 : <T extends string[]>(t: T) => [T, string[]]
>['1'] : [string]
>'1' : "1"

f2e1.toLowerCase();
>f2e1.toLowerCase() : string
>f2e1.toLowerCase : () => string
>f2e1 : string
>toLowerCase : () => string

declare const f3: <T extends string[]>(t: T) => [[T, string[]]];
>f3 : <T extends string[]>(t: T) => [[T, string[]]]
>t : T

const [[[f3e1]]] = f3(['1']);
>f3e1 : string
>f3(['1']) : [[[string], string[]]]
>f3 : <T extends string[]>(t: T) => [[T, string[]]]
>['1'] : [string]
>'1' : "1"

f3e1.toLowerCase();
>f3e1.toLowerCase() : string
>f3e1.toLowerCase : () => string
>f3e1 : string
>toLowerCase : () => string

9 changes: 0 additions & 9 deletions tests/baselines/reference/inferTupleFromBindingPattern.js

This file was deleted.

16 changes: 0 additions & 16 deletions tests/baselines/reference/inferTupleFromBindingPattern.symbols

This file was deleted.

19 changes: 0 additions & 19 deletions tests/baselines/reference/inferTupleFromBindingPattern.types

This file was deleted.

12 changes: 12 additions & 0 deletions tests/cases/compiler/inferTupleFromBindingPattern.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
// @strict: true
// @noUncheckedIndexedAccess: true, false

declare function f<T>(cb: () => T): T;
const [e1, e2, e3] = f(() => [1, "hi", true]);

// repro from #42969
declare const f2: <T extends string[]>(t: T) => [T, string[]];
const [[f2e1]] = f2(['1']);
f2e1.toLowerCase();

declare const f3: <T extends string[]>(t: T) => [[T, string[]]];
const [[[f3e1]]] = f3(['1']);
f3e1.toLowerCase();