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

for-of loop with intersection of array types produces a union of element types #39693

Open
jcalz opened this issue Jul 21, 2020 · 3 comments
Open
Labels
Fix Available A PR has been opened for this issue In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@jcalz
Copy link
Contributor

jcalz commented Jul 21, 2020

TypeScript Version: 3.9.2, 4.0.0-beta

Search Terms: intersection, for-of, union, iterable, iterator, iterate, array

Expected behavior:
When you use a for..of loop to iterate over the elements of an intersection of arrays (or maybe other iterables), what type should the elements be? I would expect either:

  • you get the same type as when you index into the array: an intersection of the element types; or
  • you get the same type as when you use the iterator method manually: the first element type because the iterator methods are overloads.

Actual behavior:
Iterating over an intersection of arrays with a for..of loop produces a union of their element types for some reason.

Related Issues:
#11961: intersection of array types results in overloaded methods (this would maybe imply overloaded iterators, but that's not happening here)

Aside:
I'm not sure why you'd want an intersection of array types in the first place; but the behavior showed up in a Stack Overflow question and I'm at a loss understanding why we get a union here.

Code

declare const arr: Array<{ a: string }> & Array<{ b: number }>;

for (const elemItr of arr) {
  // { a: string } | {b : number } 😕
  elemItr.a.toUpperCase(); // error!
  elemItr.b.toFixed(); // error!
}

// I expected either this (intersection of element types)
const elemIdx = arr[0]; // { a: string; } & { b: number; }
elemIdx.a.toUpperCase(); // okay
elemIdx.b.toFixed(); // okay

// or this (overloaded iterators giving the first element type only)
const iter = arr[Symbol.iterator];
/* const iter: {
    (): IterableIterator<{ a: string }>;
    (): IterableIterator<{ a: string }>;
} & {
    (): IterableIterator<{ b: number }>;
    (): IterableIterator<{ b: number }>;
} */
const result = arr[Symbol.iterator]().next();
if (!result.done) {
  result.value.a.toUpperCase(); // okay
  result.value.b.toFixed(); // error, expected this 
}
Output
"use strict";
for (const elemItr of arr) {
    // { a: string } | {b : number } 😕
    elemItr.a.toUpperCase(); // error!
    elemItr.b.toFixed(); // error!
}
// I expected either this (intersection of element types)
const elemIdx = arr[0]; // { a: string; } & { b: number; }
elemIdx.a.toUpperCase(); // okay
elemIdx.b.toFixed(); // okay
// or this (overloaded iterators giving the first element type only)
const iter = arr[Symbol.iterator];
/* const iter: {
    (): IterableIterator<{ a: string }>;
    (): IterableIterator<{ a: string }>;
} & {
    (): IterableIterator<{ b: number }>;
    (): IterableIterator<{ b: number }>;
} */
const result = arr[Symbol.iterator]().next();
if (!result.done) {
    result.value.a.toUpperCase(); // okay
    result.value.b.toFixed(); // error, expected this 
}
Compiler Options
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "useDefineForClassFields": false,
    "alwaysStrict": true,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "downlevelIteration": false,
    "noEmitHelpers": false,
    "noLib": false,
    "noStrictGenericChecks": false,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "esModuleInterop": true,
    "preserveConstEnums": false,
    "removeComments": false,
    "skipLibCheck": false,
    "checkJs": false,
    "allowJs": false,
    "declaration": true,
    "experimentalDecorators": false,
    "emitDecoratorMetadata": false,
    "target": "ES2017",
    "module": "ESNext"
  }
}

Playground Link: Provided

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Jul 23, 2020
@RyanCavanaugh
Copy link
Member

@ahejlsberg any opinions on how to square this circle?

@rotu
Copy link

rotu commented Jan 10, 2024

This behavior changed in between 3.9.7 and 4.0.5.

The for...of loop now infers elemItr as an intersection {a: string} & {b:number}, instead of a union.

playground

@craigphicks
Copy link

craigphicks commented Feb 16, 2024

Where did the intersection type

declare const arr: Array<{ a: string }> | Array<{ b: number }>;

come from? What is the implementation?
If the implementation is Array<{ a: string, b: number }>; then why not write it like that?

In case the actual implementation is Array<{ a: string }> | Array<{ b: number }>, then we can write it like this:

declare const arrin: Array<{ a: string }> | Array<{ b: number }>;

// This is representation is not a lie.
let arr:Array<{ a?: string, b?: number }> = arrin; // should not be error

for (const elemItr of arr) {
  elemItr.a.toUpperCase(); // should be error (because no guard)
  elemItr.b.toFixed(); // should be error (because no guard)
}

const elemIdx = arr[0];
elemIdx.a.toUpperCase(); // should be error (because no guard)
elemIdx.b.toFixed(); // should be error (because no guard)

// This is correct
const iter = arr[Symbol.iterator];
const result = arr[Symbol.iterator]().next();
if (!result.done) {
  if (result.value.a) result.value.a.toUpperCase(); // should not be error (because guard is present)
  if (result.value.b) result.value.b.toFixed(); // should not be error (because guard is present)
}

declare function g<T>(f:Array<T>):Array<T>;

g(arr);

g(arrin); // currently error (although no risk of runtime error)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fix Available A PR has been opened for this issue In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants