-
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
Proposal: typed yield expression, better generic function inference #36855
Comments
So we want a way of saying that if we yield a How could we possibly enforce this? It suggests that the actual interface of the generator changes every time you call This works the opposite way as well, we could implement a state machine with a generator such that the values yielded is directly related to the value sent into the generator, meaning inside the generator the valid type to So we actually have 2 call signatures, one for type VerySpecificGeneratorDefinition_ThatNeverReturns<
Produce,
Send,
YieldSignature extends (yielded: Produce) => Send,
NextSignature extends (sent: Send) => Produce
> = Generator<Produce, never, Send> & {next(...a: Parameters<NextSignature>): IteratorYieldResult<ReturnType<NextSignature>>} except this still assumes the exchange is only based on what is passed in and out of the generator, what about a generator that produces a So no, I'm pretty sure "Design Limitation" is still the appropriate tag for this request. |
Hi, @tadhgmister, thank you for your response.
Yes.
No. This is not about forcing consumption of generator in any way, based on this typing. What I want, is for generator function author, to enforce correct typing when writing generator function, when consumer side is known beforehand.
Again, This has specific use case in mind:
There are examples of this in JavaScript (mobx flow, co, bluebird, redux-saga), My guess to what "design limitation" would be here is in my second comment in the original post: |
As a work around, I do want to mention that /**
* creates a simple wrapper function that ensures typesafety on the yield statement for libraries.
* So when you would do something like `const x = yield val` and the type of x depends on val somehow, you can do
* ```typescript
* type YieldSig = <T>(val: T)=>T // signaure of yield expression for your case.
* const Yield = makeTypesafeYielder({} as YieldSig);
*
* function* example(){
* // we do yield* and it ensures typesafety
* const x = yield* Yield(val)
* }
* ```
* @param psudoCallSignature not used, should be passed as `{} as Sig` where Sig is the call signature of the yield enforced by your library.
*/
function makeTypesafeYielder<Produce, Send>(psudoCallSignature: (y: Produce) => Send) {
function* yielder(arg: Produce): Generator<Produce, Send, Send> {
return yield arg;
}
return yielder;
}
type Y = <T>(p: Promise<T>) => T;
const Y = makeTypesafeYielder({} as Y);
type User = { id: string; age: number };
declare function getUser(): Promise<User>;
function* userAge() {
// inferred type is correct.
const user = yield* Y(getUser());
return user.age;
} As for you suggestion, this part seems very not sound:
I understand you are only using your generators to pass to a function that will handle your generator in a particular way, but if the return signature is // returns Generator<Promise<A> | Promise<B>, [A, B], A & B>
// look ^ here - intersection.
function* resolveBoth<A,B>(pa: Promise<A>, pb: Promise<B>){
const a = yield* Y(pa);
const b = yield* Y(pb);
return [a,b] as [A,B];
} So to accurately represent our case we would need to specify the call signature of the yield expression, but still infer the send types from the actual yields inside the generator. Except we can't do this because the only place we can write details about our generator is in the return as I also want to point out that nearly every case that uses this (mobx flow, co, bluebird) including your example are being migrated to just use If the result is an Iterator object, the middleware will run that Generator function, [...]. The parent Generator will be suspended until the child Generator terminates normally, in which case the parent Generator is resumed with the value returned by the child Generator. Or until the child aborts with some error, in which case an error will be thrown inside the parent Generator. This is literally describing exactly what Just to be clear I want this feature as much as you do. But the point of typescript is to give you nice type contracts, so to ensure a lot of contracts within a generator that aren't even attempted to be enforced by the consumer code seems like a real mistake to apply to the general case. And we can't make these contracts that are enforced on both sides because of design limitations. I hope my work around works for you, have a nice day. :) |
First of all, Thank You! That said
True. // notice this also infers unknown as TYield
function *userAge() { // inferred Generator<Promise<User>, any, unknown>
const user = yield getUser(); // user is any
return user.age; // so returned type is any
}
// this works for singular types
function *userAge(): Generator<Promise<User>, number, User> {
const user = yield getUser();
return user.age;
}
== equiv
function *userAge() {
const user: User = yield getUser();
return user.age;
} // inferred Generator<Promise<User> | Promise<Account>, number, User & Account>
// the intersection may be correct, but impossible to satisfy
// can't really provide User & Account can I?
function *userAge() {
const user: User = yield getUser();
const account: Account = yield getAccount();
return user.age + account.users.length;
}
// we end up with
function *userAge(): Generator<Promise<User> | Promise<Account>, number, User | Account> {
const user = (yield getUser()) as User;
const account = (yield getAccount()) as Account;
return user.age + account.users.length;
}
const userAgeGen = userAge();
const fst = userAge.next();
const snd = userAge.next({} as Account); // no complaints
// oh no! I can send totally invalid values into our generator So I argue, while my solution doesn't add any type safety consumer side, it doesn't remove it either. It's just the same.
I didn't want to remove old Generator interface. I wanted to provide alternative, as in And why not add extra contracts if its possible, it's helpful (sometimes) and it doesn't break anything else.
Picking promises as an example was a mistake. type Parser<T> = (s: Source) => [T, Source]
type CoBind = <T>(p: Parser<T>) => T;
function defineParser<Args extends any[], Res>(
definition: (...args: Args) => Generator<???> // Generator<Parser<unknown>, Res, unknown>
): (...args: Args) => Parser<Res> {
// impl
//
} Can't really use |
That is fair and I know you won't misuse the
No you are fine. It is a good example that most people can relate to, I was only countering the argument that this is a very common use case. Just because most of the actual use cases are being replaced with I have literally implemented a version of this, the goal being that if all interface DreamArray<T> {
find(predicate: (elem: T)=>boolean): T | undefined;
find(predicate: (elem: T)=>Promise<boolean>): Promise<T | undefined>
find(predicate: (elem: T)=>boolean | Promise<boolean>): T | undefined | Promise<T | undefined>
} To accomplish this I ended up writing generators where every yield was possibly a promise and the send value was always the unwrapping of it. The difference is in my scenario I was responsible for both the producer and consumer side so I wanted both to be type safe. It didn't take long to realize that wasn't possible.
You see that is the beauty of it, if we accept that not both the producer and consumer are going to be nicely type safe in this scenario we have to choose one to be not type safe. So if the generator side writes itself to be totally type safe and the consumer can't possibly provide Conversely if the generator does type casts The issue with your suggestion for To be clear, I can see the value of this kind of feature but I would want it to fit all these criteria:
If you can think of a way to meet all of these I will back you 100%, but if no one can come up with a good solution that meets this then that is kind of the definition of "Design Limitation". Happy coding.😁 |
Thats fair.
That's an excellent point. That scenario actually didn't occur to me, maybe because I rarely see those nullish types. As a last try: function *userAge(yield: <R>(p: Promise<R>) => R) { // return type implicit
const user = yield getUser(); // correctly inferred because of yield pseudo-param
const account = yield getAccount();
return user.age + account.users.length;
} where Now there is no problem with using current rules for return type inference, typeof userAge
== (yield: <R>(p: Promise<R>) => R) => Generator<Promise<User> | Promise<Account>, number, User & Account>
ReturnType<typeof userAge>
== Generator<Promise<User> | Promise<Account>, number, User & Account> From user perspective this doesn't add much overhead, similarily as I'm curious how would You feel about this :) |
😮 Now that is a suggestion I can get behind! 🎉 👍 You will want to close this thread (since I have convinced you that your original suggestion wouldn't work) and open a new one for this. Yeah you have my support! I have a few thoughts on this below: I do think that - at least for the initial implementation - the If we did in the future have something akin to self mutating methods (so that calling Few points of validity:
|
Sure. Happy coding :) |
For reference the new issue is in #36967 |
- Attempts to find the best match for an issue with as few HTTP requests as possible. Is decoupled from how this is done, just corrals the expected calls. - Uses *effects* similar to react-saga, implemented in 'typed-effects' - Implementation was necessary because TS doesn't support strongly typed generators. (not strongly enough anyway.) See: microsoft/TypeScript#36855 (comment) - Includes a bdd wrapper, which felt like a nice way to test effects. Bit scrappy, will clean it up and publish if I keep using it.
tldr
This has exactly the same scope as #32523, except it has implementation proposal.
Search Terms
generator, generator function, yield, yield type, generic parameter typing
Setup
Proposal is to improve typing of
yield
expression, as explained here #32523.In short with code like
even though developer KNOWS
user
should be of type User, she can't denote it in TypeScript.Proposed changes
generic function return type inference
having function with signature like
<T>(p: Promise<T>) => T
and value of typePromise<number>
typescript can correctly infer type resulting from applying the value to function:
except when it can't:
I'd personally expect CallResult to work exactly like typescript call result inference.
typed yield
Developer should be able to inform typescript:
"yield has signature
<T>(p: Promise<T>) => T
inside this generator function"this would be realised by providing supplementary type
with usage like
comments
so the generator consumer facing type is correct -- we don't know Promise of what generator will yield.
This definition allows setting explicit relation between yielded value and next resumed value types, which typescript should be able to exploit, because it already does analogous things as seen here
The reason why
CallResult
doesn't work as intended is that generic parameter gets erased when the type is used as parameter.I'd argue that generic functions are really primitive to javascript, since this is a thing:
and natural signature for box is
<T>(value: T) => [T]
, and it shouldn't be erased.That said, even if this isn't implemented in general, it might be worth it for one special type,
consumed with particular syntactic feature.
The text was updated successfully, but these errors were encountered: