Skip to content

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

Open

Description

@jcalz

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

Activity

  1. RyanCavanaugh commented on Jul 23, 2020

    @RyanCavanaugh
    Member

    Anders Hejlsberg (@ahejlsberg) any opinions on how to square this circle?

  2. rotu commented on Jan 10, 2024

    @rotu

    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

  3. craigphicks commented on Feb 16, 2024

    @craigphicks

    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

Metadata

Metadata

Assignees

No one assigned

    Labels

    In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions