Stricter Generators
TypeScript 3.6 introduces stricter checking for iterators and generator functions.In earlier versions, users of generators had no way to differentiate whether a value was yielded or returned from a generator.
function* foo() {
if (Math.random() < 0.5) yield 100;
return "Finished!"
}
let iter = foo();
let curr = iter.next();
if (curr.done) {
// TypeScript 3.5 and prior thought this was a 'string | number'.
// It should know it's 'string' since 'done' was 'true'!
curr.value
}
Additionally, generators just assumed the type of yield
was always any
.
function* bar() {
let x: { hello(): void } = yield;
x.hello();
}
let iter = bar();
iter.next();
iter.next(123); // oops! runtime error!
In TypeScript 3.6, the checker now knows that the correct type for curr.value
should be string
in our first example, and will correctly error on our call to next()
in our last example.This is thanks to some changes in the Iterator
and IteratorResult
type declarations to include a few new type parameters, and to a new type that TypeScript uses to represent generators called the Generator
type.
The Iterator
type now allows users to specify the yielded type, the returned type, and the type that next
can accept.
interface Iterator<T, TReturn = any, TNext = undefined> {
// Takes either 0 or 1 arguments - doesn't accept 'undefined'
next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
return?(value?: TReturn): IteratorResult<T, TReturn>;
throw?(e?: any): IteratorResult<T, TReturn>;
}
Building on that work, the new Generator
type is an Iterator
that always has both the return
and throw
methods present, and is also iterable.
interface Generator<T = unknown, TReturn = any, TNext = unknown>
extends Iterator<T, TReturn, TNext> {
next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
return(value: TReturn): IteratorResult<T, TReturn>;
throw(e: any): IteratorResult<T, TReturn>;
[Symbol.iterator](): Generator<T, TReturn, TNext>;
}
To allow differentiation between returned values and yielded values, TypeScript 3.6 converts the IteratorResult
type to a discriminated union type:
type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;
interface IteratorYieldResult<TYield> {
done?: false;
value: TYield;
}
interface IteratorReturnResult<TReturn> {
done: true;
value: TReturn;
}
In short, what this means is that you’ll be able to appropriately narrow down values from iterators when dealing with them directly.
To correctly represent the types that can be passed in to a generator from calls to next()
, TypeScript 3.6 also infers certain uses of yield
within the body of a generator function.
function* foo() {
let x: string = yield;
console.log(x.toUpperCase());
}
let x = foo();
x.next(); // first call to 'next' is always ignored
x.next(42); // error! 'number' is not assignable to 'string'
If you’d prefer to be explicit, you can also enforce the type of values that can be returned, yielded, and evaluated from yield
expressions using an explicit return type.Below, next()
can only be called with boolean
s, and depending on the value of done
, value
is either a string
or a number
.
/**
* - yields numbers
* - returns strings
* - can be passed in booleans
*/
function* counter(): Generator<number, string, boolean> {
let i = 0;
while (true) {
if (yield i++) {
break;
}
}
return "done!";
}
var iter = counter();
var curr = iter.next()
while (!curr.done) {
console.log(curr.value);
curr = iter.next(curr.value === 5)
}
console.log(curr.value.toUpperCase());
// prints:
//
// 0
// 1
// 2
// 3
// 4
// 5
// DONE!
For more details on the change, see the pull request here.