Closed JsonFreeman closed 5 years ago
One of the good usages of yield
is communications with another process.
assert(5 === await new Coroutine<number, number, number>(async function* () {
assert(1 === (yield 0));
assert(3 === (yield 2));
return 4;
}, { size: Infinity })[Coroutine.port].connect(function* () {
assert(2 === (yield 1));
assert(4 === (yield 3));
return 5;
}));
For the async task runner / coroutine / redux-saga use cases I also think it would be very helpful to be able to describe the type for TNext
in terms of the specific type of the previously-yielded value. Basically what @Alxandr said, but I think it would be useful to express a mapping using a function type: the argument position provides spot to bind a variable for the type of the last yielded value. For example adapting @treybrisbane's example of a generator that yields promises:
function* c(): Generator<Promise<any>, string, <T>(yielded: Promise<T>) => T> {
const aNumber = yield asyncAddTwo(4); // asyncAddTwo is of type `(n: number) => Promise<number>`
const aString = yield asyncToString(aNumber * 2); // asyncToString is of type `(n: number) => Promise<string>`
return `Result was ${aString}`;
}
Instead of providing a type for TNext
this formulation provides the mapping <T>(yielded: Promise<T>) => T
. The mapping might alternatively be an intersection of function types to specify different types for the next value depending on the yielded value.
I suggest that if the type of the last yielded value does not match the input type for the mapping then the type for the next value should implicitly be never
.
(I apologize if I am repeating a suggestion that has already been made. I tried to scan the discussion so far, but I might have overlooked something.)
incorrect typing around return
type just bit me, wow this issues is old
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 error
function* gen(): IterableIterator<number> {
yield 1;
yield 2;
yield 3;
}
// need an exclamation mark because return is optional, despite the fact
// that the return method is always defined for generator objects.
gen().return!();
I'm curious: Why adding generic parameters to Iterable
and AsyncIterable
, but not to IterableIterator
and AsyncIterableIterator
?
... 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:
@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 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 #2873. This won't change with #9407. And theTNext
type in the OP above won't solve this either, since in an async runner there is generally not a singleTNext
type, but rather the type of eachyield
expression 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 eachyield
expression is generally unrelated to the types of the otheryield
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 theyield
operands. For async runners there often is no such best common type, so such generators often won't compile until at least oneyield
operand is cast toany
. E.g. thefunction *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 #9407 lands.Example code:
interface User {id; name; address} interface Order {id; date; items; supplierId} interface Supplier {id; name; phone} declare function getUser(id: number): Promise<User>; declare function getOrders(user: User): Promise<Order[]>; declare function getSupplier(id: number): Promise<Supplier>; function* foo() { let user = yield getUser(42); // user is of type 'any' let user2 = <User> user; return user2; // This return type is not preserved } function* bar() { // ERROR: No best common type exists among yield expressions let user = yield getUser(42); // user has type 'any' let orders = yield getOrders(user); // orders has type 'any' let orders2 = <Order[]> orders; let suppliers = yield orders2.map(o => getSupplier(o.supplierId)); // suppliers has type 'any' let suppliers2 = <Supplier[]> suppliers; return suppliers2; // 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
@ilbrando another alternative: https://github.com/agiledigital/typed-redux-saga
Thank you @danielnixon this is a really clever solution.
This suggestion has a few pieces:
true
andfalse
, in a fashion similar to #1003next
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.{ done: false; value: TYield; } | { done: true; value: any; }
to be compatible with generators.yield*
would only pick the value associated with done being false.yield*
expression would pick the value associated with done being true.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: