Skip to content

PSA: potential lib breaking change after iterator-helpers proposal #54481

Closed
@Josh-Cena

Description

@Josh-Cena

lib Update Request

Configuration Check

My compilation target is ESNext and my lib is the default.

Missing / Incorrect Definition

The TS lib defs for Iterator, Iterable, and IterableIterator treat them as if they are all fictitious interfaces (a.k.a. protocols). The lib def's inheritance structure looks like this:

interface Iterator<T, TReturn = any, TNext = undefined> {
    // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
    next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
    return?(value?: TReturn): IteratorResult<T, TReturn>;
    throw?(e?: any): IteratorResult<T, TReturn>;
}

interface Iterable<T> {
    [Symbol.iterator](): Iterator<T>;
}

interface IterableIterator<T> extends Iterator<T> {
    [Symbol.iterator](): IterableIterator<T>;
}

However, while Iterable and arguably IterableIterator are protocols, Iterator is an actual JS entity. See MDN docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator This has some practical issues, because what TS terms as IterableIterator is the actual JS Iterator class, but what TS terms as Iterator is what JS regards as "iterator-like" or "iterator protocol". For example, the def for new Set().values() says it returns an IterableIterator, while in fact it should return an instance of the JS Iterator instance.

This becomes more of an issue after the iterator-helpers proposal, which is going to expose Iterator as an actual global that people can use as extends. For example:

class MyClass {
  static #MyClassIterator = class extends Iterator {
    next() { ... }
  };

  [Symbol.iterator]() {
    return new MyClass.#MyClassIterator();
  }
}

Under the current lib def, #MyClassIterator won't be seen as iterable by TS, because it extends Iterator, not IterableIterator.

On the other hand, if we change the definition of Iterator to say it has [Symbol.iterator] (mirroring what happens in JS), then it will break code of the following kind:

function stepIterator(it: Iterator) {
  return it.next();
}

stepIterator({ next() {} });

Of course you could assume that almost all userland iterators are built-in and therefore inherit from Iterator and have @@iterator, but it's a breaking change nonetheless.

The question thus arises that when iterator-helpers lands, where should we put the new definitions on. Right now, I'm using core-js to polyfill these methods, and when writing declarations, I chose to put them on IterableIterator, so that new Set().values() works:

declare global {
  interface IterableIterator<T> {
    filter(
      predicate: (value: T, index: number) => unknown,
    ): IterableIterator<T>;
    map<U>(mapper: (value: T, index: number) => U): IterableIterator<U>;
    toArray(): T[];
  }
}

...but this is suboptimal, for obvious reasons (it means the Iterator class doesn't actually have these methods).

Sample Code

Documentation Link

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions