Generator: infer the type of yield expression based on yielded value
See original GitHub issueSearch 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 yield
ed 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 yield
ed 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:
- Created 4 years ago
- Reactions:118
- Comments:15 (4 by maintainers)
Top GitHub Comments
No, it is not possible. Your definition of
TestAsyncGen
doesn’t actually reflect what’s happening in the generator. The definitionnext<T>(value: T): IteratorResult<Promise<T>, void>
implies that callingnext
will with a value ofT
will produce aPromise<T>
. However, what is actually happening is that you want thePromise<T>
of one call tonext
to inform theT
of a subsequent call tonext
: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:If such a feature were to make it into the language, then you would be able to inform the
T
of a subsequent call tonext
based on the return type of a preceding call tonext
.Is it possible to go for a simpler alternative? Something like having an second, richer model for generator functions:
Rough sketch:
Libraries would be able to specify the argument type, e.g.
which would cause TS to infer the type of yield expressions here: