Repository navigation
PSA: potential lib breaking change after iterator-helpers proposal #54481
Description
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).
Activity
rbuckton commentedon Jun 1, 2023
IterableIteratoris possibly the best place for them, but in the event that is too much of a break we may introduce aNativeIterator<T>orBuiltinIterator<T>interface to avoid breaks.The biggest issue I see is that we can't properly model the type of the native
Iteratorconstructor without usingclass, because the nativeIteratorclass from the iterator helpers proposal is essentiallyabstractbecause you must still definenext:While we can model the abstract constructor using an
abstract newconstructor type, theabstract newsyntax isn't supported on construct signatures in an interface. We also have no way to indicate anabstractmethod in an interface at the moment. If we did, a non-classversion might look like this:ljharb commentedon Jun 20, 2023
I’m confused; Iterator.prototype.next exists, why would you need to define it yourself?
Josh-Cena commentedon Jun 20, 2023
...Does it? Conceptually, all iterator objects function differently exactly because they have different
next(). I don't think the proposal adds it either:ljharb commentedon Jun 20, 2023
ahhh, you're right, in this case it's IteratorHelperPrototype.next that the proposal adds.
TheCymaera commentedon Feb 14, 2024
It could be renamed to
IteratorLike, similar to the existingPromiseLikefor promises.felixfbecker commentedon Apr 5, 2024
Now that iterator helpers are shipped in Chrome and Edge, meaning it will also ship in the next NodeJS release this month (and there are polyfills available too for other engines that currently have it behind experimental flags) it would be really nice if TypeScript could tackle typing the iterator helpers. Given the current naming conflict, it is difficult to write custom typings to fill in the gap in userland (and I haven't found any successful attempts in DT or polyfill packages).
bakkot commentedon Apr 16, 2024
A concrete proposal to maybe get the ball rolling:
Iteratorinterface toIteratorLike(or some other better name). Change all internal uses ofIteratorto refer toIteratorLikeinstead, but no need to renameIterableIteratoror anything else.abstract class Iterator { ... }modeling the actualIteratorclass in the language.This is a breaking change for anyone using the
Iteratortype explicitly, but hopefully it's not that many - there's not much reason to explicitly use theIteratortype right now. There are only 176 files in DefinitelyTyped which useIterator<right now, and manual inspection reveals that more than half of those aren't referring to the TSIteratortype (e.g. x, x, x).The main way that this would break is that places which declared to need an
Iterator<T>would start giving an error if provided with{ next() {...} }, which previously worked. There do not appear to be that many cases of this. Also, types in dependencies which declare themselves to provideIterator<T>and give{ next() {...} }would appear to have.mapetc, when in fact that is missing - this gives you the wrong typing, but not in a way which causes errors for existing code.rbuckton commentedon Apr 16, 2024
The main way this would break is that any existing package that serves an
Iterator<T>for any reason would now appear to have the methods of the nativeIteratorclass when they may not, in fact, actually implement those. I still think theIterableIterator<T>type is the correct place to vend these methods from. Despite the difference in naming between theIteratorclass and theIterableIteratorinterface, we can still describe a constructor type that works:bakkot commentedon Apr 16, 2024
That's what I was saying in the second half of the bit you quoted, yeah. But that wouldn't actually cause any existing code to fail to compile (except in quite obscure cases which depend on non-existence of
mapetc at the type level), so it's only debatably "breaking". And from a quick survey of DT that happens quite rarely anyway; not many people are manually implementing iterators.IterableIteratoris an interface which can be implemented by existing things which don't provide those methods. Merely havingnextandSymbol.iteratormethods suffices to implement that interface, but doesn't give youmapetc. Are you proposing to change the definition ofIterableIteratorso that implementing it requires implementing those methods? If you're going to make a breaking change anyway, I'd think it would be better to stick closer to the actual types.Another option would be to stick with
classfor the declaration, but call it something other thanIteratorso that name doesn't end up in the type namespace (and ideally mark that name as non-exported, but I don't know if that's possible), then add a newNativeIteratorinterface for the type-level name. Something likeIncidentally, with any of these approaches, how do you update the type for Generator so that it properly inherits these new methods, without just copying them all over? Right now generators implicitly implement
IterableIteratorbut they don't actually get any methods from anywhere.rbuckton commentedon Apr 17, 2024
classalways introduces a value-space symbol in the binder, so it would always appear as if__TheActual...(or whatever we called it) is an actual value. We generally avoid that by usingvardeclarations. Theabstract new <T>(): IterableIterator<T>signature intersected with theIteratorConstructorinterface achieves the same thing without introducing a non-existing value. Albeit without the ability to define anabstractmethod. We unfortunately don't have a way to define those outside ofclasscurrently.I'm also fine with calling this
NativeIterator<T>and updating the built-ins to use this interface instead.We would just do
interface Generator extends NativeIterator.rbuckton commentedon Apr 17, 2024
We've generally discouraged implementing them directly in the past to try to set the expectation that
IterableIteratoris the thing built-ins return. I'm not sure how many explicit implementations ofIterableIteratorexist today, though.bakkot commentedon Apr 17, 2024
Yeah, that's what I was going for with the
classthing. But if you think the cost ofnextnot being abstract is worth the benefit of avoiding a new name appearing to be an actual global, that's reasonable. Shame there isn't a way to introduce the type-level class only for use in this file without polluting the global namespace.There's 77 files on DT which match
\bIterableIterator<right now, so, not many. But I'd personally still be happier withIterableIteratormeaning justIterable+Iterator. I'll get a PR starting with theNativeIteratorapproach for now; if you/the TS team decide that methods onIterableIteratorwould be better we can always switch over to that.rbuckton commentedon Apr 17, 2024
I may have a solution to use
classwithout polluting the global scope. I'll test it out tomorrow.bakkot commentedon Apr 17, 2024
Opened #58222 as a draft with the
NativeIteratorapproach, but I am happy to switch it over to use whatever other approach you'd like.felixfbecker commentedon Apr 24, 2024
FYI as of today iterator helpers are shipped in NodeJS (v22)
pspeter3 commentedon Jul 10, 2024
What’s the next step to enable iterator helpers so we can use them in Node?
bakkot commentedon Jul 10, 2024
Phips Peter (@pspeter3) You can already use them in Node. If you mean "use them with Typescript", see my PR at #58222, which has current status in the first message (namely, it's waiting on the nontrivial change in #58243).