Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why are TypeScript's IterableIterator<> and Generator<> generics slightly different?

In TypeScript (3.6.3) Generator<> is almost identical to IterableIterator<>. When Generator<> extends Iterator<>, it defaults the third generic argument (TNext) to unknown. Iterator<> by itself defaults TNext to undefined. So Generator and Iterator (and IterableIterator) don't line up as well as they might should.

let gen2:IterableIterator<string>;

function* gen1():Generator<string> {
    yield* gen2;
}

The yield* line is an error: "Cannot delegate iteration to value because the 'next' method of its iterator expects type 'undefined', but the containing generator will always send 'unknown'. ts(2766)".

Am I missing something? Is there a good reason for this?

like image 321
PreventRage Avatar asked Oct 26 '19 05:10

PreventRage


1 Answers

This is actually a very complicated issue. I do not pretend to understand it fully. But perhaps I can offer some insight.

Why did they add them?

(in this commit). Typescript decided, for better or for worse, it wants stricter type checks for generators. There are actually some valid reasons for this. Take the following example

function* foo() {
    let m = 0;
  
    while (m < 10) {
      yield m++;
    }
    
    return "done";
}

let gen = foo(),
    curr;

while(!(curr = gen.next()).done) {}

// At his point we should know that 
// curr.value is a string because curr.done is true

Here we can see the problem — we can't know if a value was returned or yielded yet by all rules of logic we should. So they introduced TReturn. TNext was introduced to:

[…] correctly check, and provide a type for, the result of a yield expression based on the next type of the generator's return type annotation (i.e. the TNext type in the Generator definition above).

Why the default values?

Now, if you have decided to make such changes you are probably going to break some code — the goal is to break as little as possible.

We must note, there are idiomatic differences in the use of the next() function in generators and non-generator iterators. As the ECMA-262 remarks for iterators.

Arguments may be passed to the next function but their interpretation and validity is dependent upon the target Iterator. The for-of statement and other common users of Iterators do not pass any arguments, so Iterator objects that expect to be used in such a manner must be prepared to deal with being called with no arguments.

Iterators are predominantly used in for-of loops which do not pass an argument to next. Indeed, it is vary rare to pass an argument to the next function (MDN even calls it a "A zero-argument function"). Therefore the only sensible option for the default value of TNext would be undefined. Making it unknown would be a great hindrance to type checking (not to mention code compiled with --strictNullChecks).

That would all be great if passing an argument to the next() function with generators wasn't a pretty common practice — it actually has a valid use case… and defined behavior in the standard:

Generator.prototype.next(value)

The next method performs the following steps:

  1. Let g be the this value.
  2. Return ?GeneratorResume(g, value, empty).

and in MDN:

The value to send to the generator.

The value will be assigned as a result of a yield expression. For example, in variable = yield expression, the value passed to the .next() function will be assigned to variable.

Not to mention, in a typical use case, the first .next() call would be called without an argument and subsequent ones will be called with. Unfortunately, there is no way to specify "optional first time, unknown subsequent times" kind of type, so I guess in light of all this they settled for unknown for the TNext in Generators.

Of course there is no perfect situation. But they have to settle for what they believe would be least problematic.

All those problems are discussed in this issue for anyone interested.

like image 179
domondo Avatar answered Nov 13 '22 10:11

domondo