-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Boolean literal types and return type propagation for generators #2983
Comments
Boolean literal types are now available in #9407. Once that is merged we can update the return type of generators and iterators. |
Does it allow to type async-like // getUser(): Promise<User>
let user = yield getUser()
// user: ? |
@s-panferov unfortunately not. You are talking about the async-runner style of generator used by things like The generator proposal (#2873) doesn't offer much typing support for async-runner generators. In particular:
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
} |
@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. |
@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 |
This hasn't actually been fixed yet. |
The issue as I see it is that without #2175, this would be a breaking change. For example, you start out fixing interface IteratorYieldResult<Y> {
done: false;
value: Y;
}
interface IteratorReturnResult<R> {
done: true;
value: R;
}
type IteratorResult<Y, R> = IteratorYieldResult<Y> | IteratorReturnResult<R> Now all of a sudden you need to introduce another type parameter to interface Iterator<Y, R> {
next(value?: any): IteratorResult<Y, R>;
return?(value?: any): IteratorResult<Y, R>;
throw?(e?: any): IteratorResult<Y, R>;
} which infects interface Iterable<Y, R> {
[Symbol.iterator](): Iterator<Y, R>;
}
interface IterableIterator<Y, R> extends Iterator<Y, R> {
[Symbol.iterator](): IterableIterator<Y, R>;
} These now break any users of interface Array<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 |
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. function iter(): IteratorResult<void, void> {
return {
done: <true>true
};
} |
With respect to what @JsonFreeman said according to the concern raised by @DanielRosenwasser, I experimented with a hypothetical typing of iterators that may return values. Currently we have this: interface IteratorResult<T> {
done: boolean;
value: T;
}
interface Iterator<T> {
next(value?: any): IteratorResult<T>;
return?(value?: any): IteratorResult<T>;
throw?(e?: any): IteratorResult<T>;
} This can be changed to: interface IteratorYieldResult<Y> {
done: false;
value: Y;
}
interface IteratorReturnResult<R> {
done: true;
value: R;
}
type IteratorResult2<T, R> = IteratorYieldResult<T> | IteratorReturnResult<R>;
// redefine IteratorResult through extended interface to preserve generic arity
type IteratorResult<T> = IteratorResult2<T, any>;
interface Iterator2<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 arity
type Iterator<T> = Iterator2<T, any, any>; Open questions:
|
Nice typing @Igorbek! I don't think the For the |
Further to @JsonFreeman's comment, there are two very different uses generators in real-world code: (1) For creating a series of values to iterate over:
(2) For async runners (e.g. using the
The latter case (async runners) should diminish with the growing awareness of async/await, but there's still a lot of existing code out there using generators this way. |
For reference, this is what Flow currently has for typing ES6 iterators and generators (from here, blog post here.). Flow is stricter than TypeScript but I suspect they had to make much of the same decisions. type IteratorResult<Yield,Return> = {
done: true,
value?: Return,
} | {
done: false,
value: Yield,
};
interface $Iterator<+Yield,+Return,-Next> {
@@iterator(): $Iterator<Yield,Return,Next>;
next(value?: Next): IteratorResult<Yield,Return>;
}
type Iterator<+T> = $Iterator<T,void,void>;
interface $Iterable<+Yield,+Return,-Next> {
@@iterator(): $Iterator<Yield,Return,Next>;
}
type Iterable<+T> = $Iterable<T,void,void>;
declare function $iterate<T>(p: Iterable<T>): T;
/* Generators */
interface Generator<+Yield,+Return,-Next> {
@@iterator(): $Iterator<Yield,Return,Next>;
next(value?: Next): IteratorResult<Yield,Return>;
return<R>(value: R): { done: true, value: R };
throw(error?: any): IteratorResult<Yield,Return>;
}
Unless the generator I think calling
Any generator that can be used with The reason Flow has the parameter to |
@jesseschalken thanks a lot for the reference how Flow typed iterator/generator.
As more I think about function* a(): Iterator2<number, string, string> {
const s = yield 1; // s is string, not string|undefined
return s;
}
for (let i of a()) {
~~~ Iterator2<number, string, string> cannot be used in for..of
}
function* b(): Iterator2<number, string, string|undefined> {
const s = yield 1; // s is string|undefined
return s || "";
}
for (let i of b()) { // ok
} |
@Igorbek what about the first push, where you need not push anything? |
Yep, as I said:
This would allow you to call However, last I checked TypeScript didn't have super type constraints, so I'm not sure how to otherwise express that in TypeScript. edit: You could just do Yep, TypeScript's function parameter bivariance is a huge pain and we're considering migrating to Flow soon for that reason among other strictness benefits.
The parameter to edit: It occurred to me that because the typing is structural and the input type for the generator is only mentioned as the parameter to |
incorrect typing around |
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 // 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!(); |
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. |
@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 Singleton types under the form of string literal types #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:
The text was updated successfully, but these errors were encountered: