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.

  1. function* foo() {
  2. if (Math.random() < 0.5) yield 100;
  3. return "Finished!"
  4. }
  5. let iter = foo();
  6. let curr = iter.next();
  7. if (curr.done) {
  8. // TypeScript 3.5 and prior thought this was a 'string | number'.
  9. // It should know it's 'string' since 'done' was 'true'!
  10. curr.value
  11. }

Additionally, generators just assumed the type of yield was always any.

  1. function* bar() {
  2. let x: { hello(): void } = yield;
  3. x.hello();
  4. }
  5. let iter = bar();
  6. iter.next();
  7. 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.

  1. interface Iterator<T, TReturn = any, TNext = undefined> {
  2. // Takes either 0 or 1 arguments - doesn't accept 'undefined'
  3. next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
  4. return?(value?: TReturn): IteratorResult<T, TReturn>;
  5. throw?(e?: any): IteratorResult<T, TReturn>;
  6. }

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.

  1. interface Generator<T = unknown, TReturn = any, TNext = unknown>
  2. extends Iterator<T, TReturn, TNext> {
  3. next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
  4. return(value: TReturn): IteratorResult<T, TReturn>;
  5. throw(e: any): IteratorResult<T, TReturn>;
  6. [Symbol.iterator](): Generator<T, TReturn, TNext>;
  7. }

To allow differentiation between returned values and yielded values, TypeScript 3.6 converts the IteratorResult type to a discriminated union type:

  1. type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;
  2. interface IteratorYieldResult<TYield> {
  3. done?: false;
  4. value: TYield;
  5. }
  6. interface IteratorReturnResult<TReturn> {
  7. done: true;
  8. value: TReturn;
  9. }

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.

  1. function* foo() {
  2. let x: string = yield;
  3. console.log(x.toUpperCase());
  4. }
  5. let x = foo();
  6. x.next(); // first call to 'next' is always ignored
  7. 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 booleans, and depending on the value of done, value is either a string or a number.

  1. /**
  2. * - yields numbers
  3. * - returns strings
  4. * - can be passed in booleans
  5. */
  6. function* counter(): Generator<number, string, boolean> {
  7. let i = 0;
  8. while (true) {
  9. if (yield i++) {
  10. break;
  11. }
  12. }
  13. return "done!";
  14. }
  15. var iter = counter();
  16. var curr = iter.next()
  17. while (!curr.done) {
  18. console.log(curr.value);
  19. curr = iter.next(curr.value === 5)
  20. }
  21. console.log(curr.value.toUpperCase());
  22. // prints:
  23. //
  24. // 0
  25. // 1
  26. // 2
  27. // 3
  28. // 4
  29. // 5
  30. // DONE!

For more details on the change, see the pull request here.