question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Generator: infer the type of yield expression based on yielded value

See original GitHub issue

Search Terms

generator, iterator, yield, type inference, redux-saga, coroutine, co, ember-concurrency

Suggestion

There are a number of JS patterns that require a generator to be able to infer the type of a yield statement based on the value that was yielded from the generator in order to be type-safe.

Since #2983 was something of a “catch-all” issue for generators, now that it has closed (🙌) there isn’t a specific issue that tracks this potential improvement, as far as I can tell. (A number of previous issues on this topic were closed in favor of that issue, e.g. #26959, #10148)

Use Cases

Use Case 1 - coroutines

A coroutine is essentially async/await implemented via generators, rather than through special language syntax. [1] There exist a number of implementations, such as Bluebird.coroutine, the co library, and similar concepts such as ember-concurrency

In all cases, it’s pretty much syntactically identical to async/await, except using function* instead of async function and yield instead of await:

type User = {name: string};
declare const getUserId: () => Promise<string>;
declare const getUser: (id: string) => Promise<User>;

const getUsername = coroutine(function*() {
    // Since a `Promise<string>` was yielded, type of `id` should be string.
    const id = yield getUserId(); 
    // Since a `Promise<User>` was yielded, type of `user` should be `User`
    const user = yield getUser(id);
    return user.name;
});

Currently, there really isn’t a better approach than explicit type annotations on every yield statement, which is completely unverified by the type-checker:

const getUsername = coroutine(function*() {
    const id: string = yield getUserId(); 
    const user: User = yield getUser(id);
    return user.name;
});

The most correct we can be right now, with TS3.6 would be to express the generator type as Generator<Promise<string> | Promise<User>, string, string | User> - but even that would require every the result of every yield to be discriminated between string and User.

It’s clearly not possible to know what the type of a yield expression is just by looking at the generator function, but ideally the types for coroutine could express the relationship between the value yielded and the resulting expression type: which is something like type ResumedValueType<Yielded> = Yielded extends Promise<infer T> ? T : Yielded.

Use Case 2 - redux-saga

redux-saga is a (fairly popular) middleware for handling asynchronous effects in a redux app. Sagas are written as generator functions, which can yield specific effect objects, and the resulting expression type (and the runtime behavior) depend on the value yielded.

For example, the call effect can be used analogously to the coroutine examples above: the generator will call the passed function, which may be asynchronous, and return the resulting value:

function* fetchUser(action: {payload: {userId: string}}) {
   try {
      const user = yield call(getUser, action.payload.userId);
      yield put({type: "USER_FETCH_SUCCEEDED", user: user});
   } catch (e) {
      yield put({type: "USER_FETCH_FAILED", message: e.message});
   }
}

The relationship between the yielded value and the resulting value is more complex as there are a lot of possible effects that could be yielded, but the resulting type could still hypothetically be determined based on the value that was yielded.

This above code is likely even a bit tricker than the coroutine as the saga generators aren’t generally wrapped in a function that could hypothetically be used to infer the yield relationship: but if it were possible to solve the coroutine case, a wrapping function for the purposes of TS could likely be introduced:

// SagaGenerator would be a type that somehow expressed the relationship 
// between the yielded values and the resulting yield expression types.
function saga<TIn, TReturn>(generator: SagaGenerator<TIn, TReturn>) { return generator; }

const fetchUser = saga(function*() {
   //...
});

I imagine this would be a difficult issue to tackle, but it could open up a lot of really expressive patterns with full type-safety if it can be handled. In any case, thanks for all the hard work on making TS awesome!


[1] As an aside, to the tangential question of “why would you use a coroutine instead of just using async/await?”. One common reason is cancellation - Bluebird promises can be cancelled, and the cancellation can propagate backwards up the promise chain, (allowing resources to be disposed or API requests to be aborted or for polling to stop, etc), which doesn’t work if there’s a native async/await layer.

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:118
  • Comments:15 (4 by maintainers)

github_iconTop GitHub Comments

6reactions
rbucktoncommented, Aug 29, 2019

Would it be possible to support this use case this way?

No, it is not possible. Your definition of TestAsyncGen doesn’t actually reflect what’s happening in the generator. The definition next<T>(value: T): IteratorResult<Promise<T>, void> implies that calling next will with a value of T will produce a Promise<T>. However, what is actually happening is that you want the Promise<T> of one call to next to inform the T of a subsequent call to next:

function start(gen) {
  const result = gen.next();
  return result.done
    ? Promise.resolve(result.value)
    : Promise.resolve(result.value).then(value => resume(gen, value));
}
function resume(gen, value) {
  const result = gen.next(value);
  return result.done
    ? Promise.resolve(result.value)
    : Promise.resolve(result.value).then(value => resume(gen, value));
}

The type system has no way to handle that today.

What you would need is something like https://github.com/microsoft/TypeScript/pull/32695#issuecomment-523733928, where calling a method can evolve the this type:

interface Start<R> {
  next<T>(): IteratorResult<Promise<T>, Promise<R>> & asserts this is Resume<T, R>;
}
interface Resume<T, R> {
  next<U>(value: T): IteratorResult<Promise<U>, Promise<R>> & asserts this is Resume<U, R>;
}

If such a feature were to make it into the language, then you would be able to inform the T of a subsequent call to next based on the return type of a preceding call to next.

5reactions
spioncommented, Sep 3, 2019

Is it possible to go for a simpler alternative? Something like having an second, richer model for generator functions:

Rough sketch:

interface BaseYieldType {
  (arg:any):any
}

interface Coroutine<
  Args extends any[],
  RetType,
  BaseCoroutineProduction,
  YieldType extends BaseYieldType
> {
  (...args:Args):Generator<BaseCoroutineProduction, RetType, any>
  __yield: YieldType; // replace with some TS only symbol maybe
}

Libraries would be able to specify the argument type, e.g.


type AwaitingYield = <T>(p:Promise<T>) => T;

declare function coroutine<Args extends any[], Ret>(
  genfn: Coroutine<Args, Ret, Promise<unknown>, AwaitingYield>
) : (...args:Args) => Ret;


which would cause TS to infer the type of yield expressions here:

// This type cast wont be necessary, instead the type will be
// inferred from the specified Coroutine<...> argument to coroutine()

let x = coroutine(function* f() {
  let a = yield Promise.resolve(1);
  let b = yield Promise.resolve(true);
  return Promise.resolve('a')
}) as Coroutine<[], Promise<string>, Promise<any>, AwaitingYield>)

Read more comments on GitHub >

github_iconTop Results From Across the Web

Yield in Python Tutorial: Generator & Yield vs Return Example
Python yield returns a generator object. Generators are special functions that have to be iterated to get the values. The yield keyword converts ......
Read more >
Documentation - TypeScript 3.6
To correctly represent the types that can be passed in to a generator from calls to next() , TypeScript 3.6 also infers certain...
Read more >
How to determine the yield / send / return values for any ...
The yield value seems easy, as I assume one can just do type(next(<generator>)) , but the others aren't so clear. example. For example,...
Read more >
TypeScript and Redux Sagas - Understandable
Ok, so the first type parameter to the Generator type is the type of yield ed values, and the second type is the...
Read more >
generators - The Rust Unstable Book
The Generator trait ... The Generator::Yield type is the type of values that can be yielded with the yield statement. The Generator::Return type...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Hashnode Post

No results found