You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Boolean literal types and return type propagation for generators #2983
Implement type guards for booleans. Essentially, control flow constructs like if-else, while, for, etc would be subject to a type guard if their guard expression is a boolean.
Now we can update generators to have a next method that returns { done: false; value: TYield; } | { done: true; value: TReturn; }, where TYield is inferred from the yield expressions of a generator, and TReturn is inferred from the return expressions of a generator.
Iterators would return { done: false; value: TYield; } | { done: true; value: any; } to be compatible with generators.
for-of, spread, array destructuring, and the type yielded by yield* would only pick the value associated with done being false.
The value of a yield* expression would pick the value associated with done being true.
Introduce a Generator type that can be used to track the desired type of a yield expression. This would be TNext, and would be the type of the parameter for the next method on a generator.
The generator type would look something like this:
Stanislav Panferov (@s-panferov) unfortunately not. You are talking about the async-runner style of generator used by things like co right?
The generator proposal (#2873) doesn't offer much typing support for async-runner generators. In particular:
All yield expressions are typed as any, as you can see in the example comments below. This is actually a very complex problem and I tried tackling it with some ideas which are all there in Proposal for generators design #2873. This won't change with Number, enum, and boolean literal types #9407. And the TNext type in the OP above won't solve this either, since in an async runner there is generally not a single TNext type, but rather the type of each yield expression is a function of the type of that yield's operand (eg Promise<T> maps to T, Array<Promise<T>> maps to T[], etc). In particular the type of each yield expression is generally unrelated to the types of the other yield expressions in the body in an async-runner generator function.
The compiler infers a single TYield type that must be the best common type among all the yield operands. For async runners there often is no such best common type, so such generators often won't compile until at least one yield operand is cast to any. E.g. the function *bar example below doesn't compile for this reason.
Return types of generators are currently not tracked because without boolean literals, they can't be separated from the TYield type. This is what will be solvable once Number, enum, and boolean literal types #9407 lands.
Example code:
interfaceUser{id;name;address}interfaceOrder{id;date;items;supplierId}interfaceSupplier{id;name;phone}declarefunctiongetUser(id: number): Promise<User>;declarefunctiongetOrders(user: User): Promise<Order[]>;declarefunctiongetSupplier(id: number): Promise<Supplier>;function*foo(){letuser=yieldgetUser(42);// user is of type 'any'letuser2=<User>user;returnuser2;// This return type is not preserved}function*bar(){// ERROR: No best common type exists among yield expressionsletuser=yieldgetUser(42);// user has type 'any'letorders=yieldgetOrders(user);// orders has type 'any'letorders2=<Order[]>orders;letsuppliers=yieldorders2.map(o=>getSupplier(o.supplierId));// suppliers has type 'any'letsuppliers2=<Supplier[]>suppliers;returnsuppliers2;// This return type is not preserved}
Reacted by JeroenNelen, Simon Friis Vindum, Aleksandr Chernyavenko, timm-aym, Dan Wilt, Berkay Yıldız, roman, Yijian Wang, 流浪大法师, Artur Udalov and 1 more
All yield expressions are typed as any, as you can see in the example comments below. This is actually a very complex problem and I tried tackling it with some ideas which are all there in #2873. This won't change with #9407. And the TNext type in the OP above won't solve this either, since in an async runner there is generally not a single TNext type, but rather the type of each yield expression is a function of the type of that yield's operand (eg Promise maps to T, Promise[] maps to Promise<T[]>, etc). In particular the type of each yield expression is generally unrelated to the types of the other yield expressions in the body in an async-runner generator function.
Do you know if there is a tracking issue for this use-case? I think we definitely need to continue discussion, because this use-case is quite common and becomes more and more popular.
Stanislav Panferov (@s-panferov) no problem. I think there's just #2873. There's quite a lot of discussion about the async-runner use-case in there, but I think that the team wanted to focus on getting simpler use cases working initially. Since that issue is now closed, I guess you could open a new issue focused specifically on better typing for co-style generators.
These now break any users of Iterators. For instance, Array's members needed to be fixed up to:
interfaceArray<T>{/** Iterator */[Symbol.iterator](): IterableIterator<T,undefined>;/** * Returns an array of key, value pairs for every entry in the array */entries(): IterableIterator<[number,T],undefined>;/** * Returns an list of keys in the array */keys(): IterableIterator<number,undefined>;/** * Returns an list of values in the array */values(): IterableIterator<T,undefined>;}
Yes I remember our long discussion about this. The tricky bit is that many users will just want to use for-of, spread and rest, which never use the R type. Those users will not care about R, only Y. Then there are some users who will call the iterator methods explicitly, and they will care about the R type. The art is in serving both use cases simultaneously. I think there needs to be a type with two type parameters, and another type with only one, where the second type argument is any.
I feel definitions using literal types is too complex for common interfaces because we need to explicitly assert a boolean literal type for now. We need more easy ways to use literal types.
This can be changed to: Sorry for names end with 2, that's just for illustration
interfaceIteratorYieldResult<Y>{done: false;value: Y;}interfaceIteratorReturnResult<R>{done: true;value: R;}typeIteratorResult2<T,R>=IteratorYieldResult<T>|IteratorReturnResult<R>;// redefine IteratorResult through extended interface to preserve generic aritytypeIteratorResult<T>=IteratorResult2<T,any>;interfaceIterator2<T,R,I>{next(value?: I): IteratorResult2<T,R>;return?(value: R): IteratorResult2<T,R>;throw?(e?: any): IteratorResult2<T,R>;}// redefine Iterator through extended interface to preserve generic aritytypeIterator<T>=Iterator2<T,any,any>;
Open questions:
next(value: I) or next(value?: I)
if value is optional then we will not be able to enforce strict type I in yield expression in generators 👎
if value were not be optional then to allow next() we'd need to either type I | undefined or require consumer to work around like next(undefined as I) 👎
return(value: R) strictly requires value to be passed, right?
I don't think the return parameter should be required. I get the impression that the return method is largely for cleanup, just executing any finally clauses that correspond to try/catch blocks surrounding the execution point (for a generator in particular). Unless you have a yield expression in one of the finally blocks, the consumer will already have the value they want returned.
For the next parameter, I think it has to be optional. Consider the language constructs for iterating (for-of, spread, etc). None of those constructs pass a parameter. If the generator really needs the parameter to be passed, then I think it is misleading to call it an iterator. It might be better to have a separate generator type for that, since the consumer will have to interact with the object in a different way.
incorrect typing around return type just bit me, wow this issues is old
Reacted by Dziamid, Kirill Agalakov, Joe Calzaretta, Yifei Wang, Dom DiCicco, David Piper, Tylor Steinberger, ExE Boss, Fabrizio, Richard Simpson and 12 more
If this is gonna be worked on, I think a nice easy win separate from typing the return value of generators would be to make the return and throw methods non-optional.
I’m writing a lot of code like:
// having this function return an iterator type with non-optional `return` and// `throw` results in a type errorfunction*gen(): IterableIterator<number>{yield1;yield2;yield3;}// need an exclamation mark because return is optional, despite the fact// that the return method is always defined for generator objects.gen().return!();
Stanislav Panferov (@s-panferov) unfortunately not. You are talking about the async-runner style of generator used by things like co right?
The generator proposal (#2873) doesn't offer much typing support for async-runner generators. In particular:
All yield expressions are typed as any, as you can see in the example comments below. This is actually a very complex problem and I tried tackling it with some ideas which are all there in Proposal for generators design #2873. This won't change with Number, enum, and boolean literal types #9407. And the TNext type in the OP above won't solve this either, since in an async runner there is generally not a single TNext type, but rather the type of each yield expression is a function of the type of that yield's operand (eg Promise<T> maps to T, Array<Promise<T>> maps to T[], etc). In particular the type of each yield expression is generally unrelated to the types of the other yield expressions in the body in an async-runner generator function.
The compiler infers a single TYield type that must be the best common type among all the yield operands. For async runners there often is no such best common type, so such generators often won't compile until at least one yield operand is cast to any. E.g. the function *bar example below doesn't compile for this reason.
Return types of generators are currently not tracked because without boolean literals, they can't be separated from the TYield type. This is what will be solvable once Number, enum, and boolean literal types #9407 lands.
Example code:
interfaceUser{id;name;address}interfaceOrder{id;date;items;supplierId}interfaceSupplier{id;name;phone}declarefunctiongetUser(id: number): Promise<User>;declarefunctiongetOrders(user: User): Promise<Order[]>;declarefunctiongetSupplier(id: number): Promise<Supplier>;function*foo(){letuser=yieldgetUser(42);// user is of type 'any'letuser2=<User>user;returnuser2;// This return type is not preserved}function*bar(){// ERROR: No best common type exists among yield expressionsletuser=yieldgetUser(42);// user has type 'any'letorders=yieldgetOrders(user);// orders has type 'any'letorders2=<Order[]>orders;letsuppliers=yieldorders2.map(o=>getSupplier(o.supplierId));// suppliers has type 'any'letsuppliers2=<Supplier[]>suppliers;returnsuppliers2;// This return type is not preserved}
As it is not an easy task to make TypeScript able to handle generator functions like they are used by Redux Saga, I have come up with a workaround that works for me.
You can see it here: https://github.com/ilbrando/redux-saga-typescript
Reacted by CallMeLaNN and 流浪大法师
Reacted by Brian Kim, CallMeLaNN, Max, Eugene Oskin, 流浪大法师 and Damilola Majek
Activity
ahejlsberg commentedon Jun 29, 2016
Boolean literal types are now available in #9407. Once that is merged we can update the return type of generators and iterators.
s-panferov commentedon Jul 5, 2016
Does it allow to type async-like
yieldexpressions in libraries likecoorreact-saga?yortus commentedon Jul 5, 2016
Stanislav Panferov (@s-panferov) unfortunately not. You are talking about the async-runner style of generator used by things like
coright?The generator proposal (#2873) doesn't offer much typing support for async-runner generators. In particular:
yieldexpressions are typed asany, as you can see in the example comments below. This is actually a very complex problem and I tried tackling it with some ideas which are all there in Proposal for generators design #2873. This won't change with Number, enum, and boolean literal types #9407. And theTNexttype in the OP above won't solve this either, since in an async runner there is generally not a singleTNexttype, but rather the type of eachyieldexpression is a function of the type of thatyield's operand (egPromise<T>maps toT,Array<Promise<T>>maps toT[], etc). In particular the type of eachyieldexpression is generally unrelated to the types of the otheryieldexpressions in the body in an async-runner generator function.TYieldtype that must be the best common type among all theyieldoperands. For async runners there often is no such best common type, so such generators often won't compile until at least oneyieldoperand is cast toany. E.g. thefunction *barexample below doesn't compile for this reason.TYieldtype. This is what will be solvable once Number, enum, and boolean literal types #9407 lands.Example code:
s-panferov commentedon Jul 5, 2016
Troy Gerwien (@yortus) big thanks for the clarification!
Do you know if there is a tracking issue for this use-case? I think we definitely need to continue discussion, because this use-case is quite common and becomes more and more popular.
yortus commentedon Jul 5, 2016
Stanislav Panferov (@s-panferov) no problem. I think there's just #2873. There's quite a lot of discussion about the async-runner use-case in there, but I think that the team wanted to focus on getting simpler use cases working initially. Since that issue is now closed, I guess you could open a new issue focused specifically on better typing for
co-style generators.DanielRosenwasser commentedon Aug 15, 2016
This hasn't actually been fixed yet.
DanielRosenwasser commentedon Aug 15, 2016
The issue as I see it is that without #2175, this would be a breaking change. For example, you start out fixing
IteratorResult:Now all of a sudden you need to introduce another type parameter to
Iterator:which infects
Iterable&IterableIterator:These now break any users of
Iterators. For instance,Array's members needed to be fixed up to:JsonFreeman commentedon Aug 15, 2016
Yes I remember our long discussion about this. The tricky bit is that many users will just want to use for-of, spread and rest, which never use the R type. Those users will not care about R, only Y. Then there are some users who will call the iterator methods explicitly, and they will care about the R type. The art is in serving both use cases simultaneously. I think there needs to be a type with two type parameters, and another type with only one, where the second type argument is
any.falsandtru commentedon Sep 1, 2016
I feel definitions using literal types is too complex for common interfaces because we need to explicitly assert a boolean literal type for now. We need more easy ways to use literal types.
Igorbek commentedon Oct 5, 2016
With respect to what Jason Freeman (@JsonFreeman) said according to the concern raised by Daniel Rosenwasser (@DanielRosenwasser), I experimented with a hypothetical typing of iterators that may return values.
Currently we have this:
This can be changed to:
Sorry for names end with
2, that's just for illustrationOpen questions:
next(value: I)ornext(value?: I)valueis optional then we will not be able to enforce strict typeIinyieldexpression in generators 👎valuewere not be optional then to allownext()we'd need to either typeI | undefinedor require consumer to work around likenext(undefined as I)👎return(value: R)strictly requiresvalueto be passed, right?JsonFreeman commentedon Oct 5, 2016
Nice typing Igor Oleinikov (@Igorbek)!
I don't think the
returnparameter should be required. I get the impression that thereturnmethod is largely for cleanup, just executing anyfinallyclauses that correspond to try/catch blocks surrounding the execution point (for a generator in particular). Unless you have a yield expression in one of the finally blocks, the consumer will already have the value they want returned.For the
nextparameter, I think it has to be optional. Consider the language constructs for iterating (for-of, spread, etc). None of those constructs pass a parameter. If the generator really needs the parameter to be passed, then I think it is misleading to call it an iterator. It might be better to have a separate generator type for that, since the consumer will have to interact with the object in a different way.45 remaining items
zpdDG4gta8XKpMCd commentedon Mar 7, 2019
incorrect typing around
returntype just bit me, wow this issues is oldbrainkim commentedon Jun 1, 2019
If this is gonna be worked on, I think a nice easy win separate from typing the return value of generators would be to make the
returnandthrowmethods non-optional.I’m writing a lot of code like:
oguimbal commentedon Jul 30, 2019
I'm curious: Why adding generic parameters to
IterableandAsyncIterable, but not toIterableIteratorandAsyncIterableIterator?... i have been waiting for this feature for a long time, but not extending iterableIterators forbids using strongly typed
yield*result.Works using this hack, though:
ilbrando commentedon Jan 15, 2020
As it is not an easy task to make TypeScript able to handle generator functions like they are used by Redux Saga, I have come up with a workaround that works for me.
You can see it here: https://github.com/ilbrando/redux-saga-typescript
danielnixon commentedon Jan 15, 2020
ilbrando another alternative: https://github.com/agiledigital/typed-redux-saga
ilbrando commentedon Jan 16, 2020
Thank you Daniel Nixon (@danielnixon) this is a really clever solution.