Skip to content

typescript can't infer destructuring of array properly #32465

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
deepkolos opened this issue Jul 18, 2019 · 7 comments · Fixed by #36861
Closed

typescript can't infer destructuring of array properly #32465

deepkolos opened this issue Jul 18, 2019 · 7 comments · Fixed by #36861
Assignees
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue

Comments

@deepkolos
Copy link

deepkolos commented Jul 18, 2019

TypeScript Version: 3.5.1

Search Terms: infer array destruction

Code

// A *self-contained* demonstration of the problem follows...
// Test this by running `tsc` on the command-line, rather than through another build tool such as Gulp, Webpack, etc.
function formatValue(value: string): [string, boolean] {
  const hasExpandString = /\%[^\%]*\%/i;
  return [value.replace(/^[\'\"].*[\'\"]$/, ''), hasExpandString.test(value)];
}

function formatOptions(options: string[]): [string, string, string, boolean] {
  let key, value, operator;
  switch (options.length) {
    case 2:
      [key, value] = options;
      return [key, '=', ...formatValue(value)]; 
      //                       ^
      // Type 'string | boolean' is not assignable to type 'string'.
      // Type 'false' is not assignable to type 'string'.
    default:
      throw new Error('argument error');
  }
}

Expected behavior: after destruction of array ts can infer [key, '=', ...formatValue(value)] to be [string, string, string, boolean] with formatValue(string): [string, boolean]

Actual behavior: got error
Type 'string | boolean' is not assignable to type 'string'.
Type 'false' is not assignable to type 'string'.

Playground Link: https://www.typescriptlang.org/play/index.html#code/GYVwdgxgLglg9mABMOAnAtgQygNUwGxAFMAKANwOIC5EBnKVGMAcwEoaBtexlgGkQBGcOPiKYwAXUQBvAFCJEEBPUQALTLQCiADwAO4gCYBlBk2aIAvIgD0AHQCkHAHoOJAKgfWYAbnmJURFAgqEgcFIREAHQBuviYEKTWThy2AOS2AEQSkW4p6VkAJNb8qams-OpaeoYmPMyRUET05JRErBK+AL6ysqCQsAjIaFhQAPK6A2C0JHAT8FM03GYcEuyIXKZ8dJvM-EtbQiJikjJ+olCIANZEAJ784cT8s0So2Gi+CrQA7jBQEKqIGZzZSRUQsKCqVinBQKCAaIiIABMVD8MPW1zuiAeRCkVlmk1oHzR-kCwVCGJKFlS-EitJQGGweAiLQi7SJsPhiAAzCjiejbk9dC83qh7q1cYh8fNCaiYTBgIDnq8oGhLBYrKkANSpRAAH11kqFytV6o1VNYsrRASCIX5mKVIppdOGjNaLOIbMtiCI+FoCIhqDgX0QYCIwc0qEDqBIqQdKtQgCvlQC-AYBrDUAmYqWfja1jsxAGIjATAgfBQXnEgNBkNhxARqMxwBDyoAHU3TgExUwD30WUid1OkA

Related Issues: #5296

@russelldavis
Copy link
Contributor

To provide some clarification here, the term is "destructuring" (not "destruction"). And, to be even more precise, it's actually a spread operation. But I agree this is an issue. (@deepkolos it might help to update your title and description.)

@RyanCavanaugh RyanCavanaugh added the Bug A bug in TypeScript label Jul 26, 2019
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.7.0 milestone Jul 26, 2019
@RyanCavanaugh
Copy link
Member

Simpler repro:

declare const sb: [string, boolean];
// Shouldn't error
const k: [number, string, boolean] = [1, ...sb];

@deepkolos deepkolos changed the title typescript can't infer destruction of array properly typescript can't infer destructuring of array properly Jul 29, 2019
@micheleangioni
Copy link

I ran into this problem as well, it would be nice to have it fixed :)

@dianafa
Copy link

dianafa commented Dec 18, 2019

Same issue with the code:

        const testCases = [
            [['1', '2'], 3],
        ];

        testCases.forEach(([input, expected] :[string[], number]) =>
            test('works', () => {
                expect(computeSum(input)).toEqual(expected);
            }),
        );

Gives error:

TS2345: Argument of type '([input, expected]: [string[], number]) => void' is not assignable to parameter of type '(value: (number | string[])[], index: number, array: (number | string[])[][]) => void'.
  Types of parameters '__0' and 'value' are incompatible.
    Type '(number | string[])[]' is missing the following properties from type '[string[], number]': 0, 1

The workaround was to use objects instead. Object destructuring works good 👍

        const testCases = [
            {input: ['1', '2'], expected: 3},
        ];

        testCases.forEach(({input, expected} :  { input: string[], expected: number }) =>
            test('works', () => {
                expect(computeSum(input)).toEqual(expected);
            }),
        );

but sometimes the code is not that flexible to change :)

@tylervick
Copy link

+1 to this - it's especially annoying in my common use case of passing down the return value from useState in React.

elibarzilay added a commit to elibarzilay/TypeScript that referenced this issue Feb 18, 2020
elibarzilay added a commit to elibarzilay/TypeScript that referenced this issue Feb 18, 2020
elibarzilay added a commit to elibarzilay/TypeScript that referenced this issue Feb 18, 2020
elibarzilay added a commit to elibarzilay/TypeScript that referenced this issue Feb 27, 2020
@elibarzilay elibarzilay added the Fix Available A PR has been opened for this issue label Feb 27, 2020
elibarzilay added a commit to elibarzilay/TypeScript that referenced this issue Feb 27, 2020
Fixes microsoft#32465.

After this was done, I continued to extend the implementation to handle
TupleLike types but it'ss broken since JS doesn't allow splicing
TupleLike values into array literals so pulled that out, and instead
keeping it for reference below.  (It Includes tests, which are broken
too.)

modified   src/compiler/checker.ts
@@ -22268,6 +22268,21 @@ namespace ts {
                         else hasNonEndingSpreadElement = true;
                     }
                 }
+                else if (spreadType && isTupleLikeType(spreadType)) {
+                    let i = 0, tupleEltType: Type | undefined;
+                    while (tupleEltType = getTypeOfPropertyOfType(spreadType, "" + i as __String)) {
+                        elementTypes.push(tupleEltType);
+                        i++;
+                    }
+                    const stringIndexInfo = getIndexInfoOfType(spreadType, IndexKind.String);
+                    const numberIndexInfo = getIndexInfoOfType(spreadType, IndexKind.Number);
+                    if (stringIndexInfo || numberIndexInfo) {
+                        if (stringIndexInfo) elementTypes.push(stringIndexInfo.type);
+                        if (numberIndexInfo) elementTypes.push(numberIndexInfo.type);
+                        if (i === elementCount - 1) hasEndingSpreadElement = true;
+                        else hasNonEndingSpreadElement = true;
+                    }
+                }
                 else {
                     if (inDestructuringPattern && spreadType) {
                         // Given the following situation:
new file   tests/cases/compiler/spliceTupleLikesWIntegers.ts
@@ -0,0 +1,23 @@
+declare const sb: { [0]: string, [1]: boolean };
+
+let k1: [number, string, boolean];
+k1 = [1, ...sb];
+
+let k2: [number, string, boolean, number];
+k2 = [1, ...sb, 1];
+
+// declare const sb_: [string, ...boolean[]];
+
+// let k3: [number, string, ...boolean[]];
+// k3 = [1, ...sb_];
+
+// declare const sbb_: [string, boolean, ...boolean[]];
+
+// let k4: [number, string, ...boolean[]];
+// k4 = [1, ...sbb_];
+
+// let k5: [number, string, boolean, ...boolean[]];
+// k5 = [1, ...sbb_];
+
+// let k6: [number, string, boolean, boolean, ...boolean[]];
+// k6 = [1, ...sbb_];
new file   tests/cases/compiler/spliceTupleLikesWStrings.ts
@@ -0,0 +1,23 @@
+declare const sb: { 0: string, 1: boolean };
+
+let k1: [number, string, boolean];
+k1 = [1, ...sb];
+
+let k2: [number, string, boolean, number];
+k2 = [1, ...sb, 1];
+
+declare const sb_: { 0: string, [s: string]: (boolean|string) };
+
+let k3: [number, string, ...(boolean|string)[]];
+k3 = [1, ...sb_];
+
+declare const sbb_: { 0: string, 1: boolean, [s: string]: (boolean|string) };
+
+let k4: [number, string, boolean, ...(boolean|string)[]];
+k4 = [1, ...sbb_];
+
+// let k5: [number, string, boolean, ...(boolean|string)[]];
+// k5 = [1, ...sbb_];
+
+// let k6: [number, string, boolean, boolean, ...(boolean|string)[]];
+// k6 = [1, ...sbb_];
elibarzilay added a commit that referenced this issue Feb 27, 2020
Fixes #32465.

After this was done, I continued to extend the implementation to handle
TupleLike types but it'ss broken since JS doesn't allow splicing
TupleLike values into array literals so pulled that out, and instead
keeping it for reference below.  (It Includes tests, which are broken
too.)

modified   src/compiler/checker.ts
@@ -22268,6 +22268,21 @@ namespace ts {
                         else hasNonEndingSpreadElement = true;
                     }
                 }
+                else if (spreadType && isTupleLikeType(spreadType)) {
+                    let i = 0, tupleEltType: Type | undefined;
+                    while (tupleEltType = getTypeOfPropertyOfType(spreadType, "" + i as __String)) {
+                        elementTypes.push(tupleEltType);
+                        i++;
+                    }
+                    const stringIndexInfo = getIndexInfoOfType(spreadType, IndexKind.String);
+                    const numberIndexInfo = getIndexInfoOfType(spreadType, IndexKind.Number);
+                    if (stringIndexInfo || numberIndexInfo) {
+                        if (stringIndexInfo) elementTypes.push(stringIndexInfo.type);
+                        if (numberIndexInfo) elementTypes.push(numberIndexInfo.type);
+                        if (i === elementCount - 1) hasEndingSpreadElement = true;
+                        else hasNonEndingSpreadElement = true;
+                    }
+                }
                 else {
                     if (inDestructuringPattern && spreadType) {
                         // Given the following situation:
new file   tests/cases/compiler/spliceTupleLikesWIntegers.ts
@@ -0,0 +1,23 @@
+declare const sb: { [0]: string, [1]: boolean };
+
+let k1: [number, string, boolean];
+k1 = [1, ...sb];
+
+let k2: [number, string, boolean, number];
+k2 = [1, ...sb, 1];
+
+// declare const sb_: [string, ...boolean[]];
+
+// let k3: [number, string, ...boolean[]];
+// k3 = [1, ...sb_];
+
+// declare const sbb_: [string, boolean, ...boolean[]];
+
+// let k4: [number, string, ...boolean[]];
+// k4 = [1, ...sbb_];
+
+// let k5: [number, string, boolean, ...boolean[]];
+// k5 = [1, ...sbb_];
+
+// let k6: [number, string, boolean, boolean, ...boolean[]];
+// k6 = [1, ...sbb_];
new file   tests/cases/compiler/spliceTupleLikesWStrings.ts
@@ -0,0 +1,23 @@
+declare const sb: { 0: string, 1: boolean };
+
+let k1: [number, string, boolean];
+k1 = [1, ...sb];
+
+let k2: [number, string, boolean, number];
+k2 = [1, ...sb, 1];
+
+declare const sb_: { 0: string, [s: string]: (boolean|string) };
+
+let k3: [number, string, ...(boolean|string)[]];
+k3 = [1, ...sb_];
+
+declare const sbb_: { 0: string, 1: boolean, [s: string]: (boolean|string) };
+
+let k4: [number, string, boolean, ...(boolean|string)[]];
+k4 = [1, ...sbb_];
+
+// let k5: [number, string, boolean, ...(boolean|string)[]];
+// k5 = [1, ...sbb_];
+
+// let k6: [number, string, boolean, boolean, ...(boolean|string)[]];
+// k6 = [1, ...sbb_];
@keithbro
Copy link

I think this is still an issue. With the following code:

const f = () => [[1], 'word'];
const [arr, str] = f();
console.log({ arr, str })
arr.map((i) => i);

I get an issue when testing against the nightly build on https://www.typescriptlang.org/play

Property 'map' does not exist on type 'string | number[]'.
  Property 'map' does not exist on type 'string'.(2339)

@elibarzilay
Copy link
Contributor

@keithbro, the problem here is the inferred type of f, which TS assumes to be an array and therefore results in the union that you see.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants