Skip to content
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

Feature request: typed yield expression #36967

Open
5 tasks done
rerion opened this issue Feb 23, 2020 · 7 comments
Open
5 tasks done

Feature request: typed yield expression #36967

rerion opened this issue Feb 23, 2020 · 7 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@rerion
Copy link

rerion commented Feb 23, 2020

Search Terms

generator, iterator, yield, type inference, co, redux-saga

Related Issues

#32523

Suggestion

Allow passing of yield operator signature to generator functions

declare function fetchUser(): Promise<User>;
function asyncAction*(yield: <R>(p: Promise<R>) => R) {
  const user = yield fetchUser(); // user type inferred to User based on yield param
  // impl
}

where yield inside params list is a pseudo-parameter similar to this.

Use Cases

Discussion of use cases is given in #32523.

My intended use is case is emulating do notation for monads with generator functions.
This implies I would normally have some generator runner that does composition defined before writing generator function, so type of expression at which generator resumes is trivially inferrable based on yielded value.

Discussion

The hard thing in typing yield expressions in is that resumed type does not depend only on passed parameters and generator type, but also on context in which the generator is run.

There is a precedent in TypeScript with similar situation -- this parameter in functions.
The situation is analogous -- what this behaves like depends on which object function is member of.

The proposal is to allow declaring contextual info about yield inside generator functions, by introducing appropriate pseudo-param.

I could see this working in two ways:

  • yield param gets erased early, it does not affect generator function signature,
    it serves merely as a contract for type-checking / inferrence inside body
function asyncAction*(yield: <R>(p: Promise<R>) => R) {
  // impl
}
typeof asyncAction == () => Generator<...>
  • yield param is preserved as type info like this param.
    then it is always present on types of shape (yield: ...) => Generator<> and defaults to (a: any) => any.
function asyncAction*(yield: <R>(p: Promise<R>) => R) {
  // impl
}
typeof asyncAction == (yield: <R>(p: Promise<R>) => R) => Generator<...>

Additionaly

  1. signature of yield must extend (a: any) => any and defaults to it
function asyncAction*(yield: number) {
  // impl
}
// Error: signature must be of shape (any => any)
  1. yielded values inside generator function body must be assignable to argument of yield signature
function asyncAction*(yield: (s: string) => void) {
  yield 7; // Error: number is not assignable to string
}
  1. this parameter has no effect on Generator<> return type, whether declared or
    other than obvious coherence guaranteed by (2)

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
    thats an opt-in feature
  • This wouldn't change the runtime behavior of existing JavaScript code
    type level feature, doesn't affect runtime behavior
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@tadhgmister
Copy link

tadhgmister commented Feb 24, 2020

To clarify intent: Currently without an explicit return type annotation all yield expressions give type any and no check is done on the yielded value. (Both of these behaviours are completely correct.) This suggestion is to have a way to specify that yield(v) should be type checked as if yield has a known function signature. No other current behaviour would be affected at all by this suggestion.

For context, a lot of the discussion on #30790 is requesting a feature like this. The conversation is well summed up as this:

  1. How do we write code for libraries like co or redux-saga?
  2. you would need a way to relate the yielded type to the next type, which we currently don't support
  3. writing the relationship for a library is easy, shame I can't tell it to typescript.
  4. Maybe we'd need higher order generics?

Higher order generics kind of went out the window being replaced with the much more useful Type inference in conditional types #21496. This suggestion composes well with all already existing constructs, it's just a way to let yield(v) be type checked like it was any other function.

@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Feb 24, 2020
@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Feb 24, 2020
@tomwidmer
Copy link

Does this extend to multiple relationships between yielded types and the type of the yield expression? Can you do:

interface CallExpression<Args extends any[], R> {
  type: "__call__;
  f: (...args: Args) => R;
  args: Args;
}

function call<Args extends any[], R>(f: (...args: Args) => R, ...args: Args): CallExpression<Args, R> {
  return {type: "__call__", f, args};
}

type YieldMapping = {
  // as many overloads as you like:
  <R>(p: Promise<R>) => R;
  <Args extends any[], R>(call: CallExpression<Args, R>) => R;
};

function f(a: number) { return a + 1 };

function *mygenerator(yield: YieldMapping): number {
  const n = yield Promise.resolve(10); // n inferred as type number
  return yield call(f, n); // return inferred as type number, since R is inferred from CallExpression
}

You'd need something like that to make redux saga type-check. I think this proposal seems potentially workable. I'm currently steering away from generators in TS purely because of the lack of yield type-checking, but they are much more powerful than async/await.

@mikecann
Copy link

For others that might have landed on this issue trying to find a solution. MobX State Tree uses generator functions for its async actions structure. I have written a library that helps with the typing by turning the flow into a continuation style syntax.

https://github.com/mikecann/mst-flow-pipe#readme

It means that each step (yeild) in the function is type safe once more. Not a perfect solution but it works for now.

aaronpowell added a commit to aaronpowell/azure-functions-durable-js that referenced this issue Jun 16, 2020
This is required as TypeScript currently doesn't support implicit casting of yield return values: microsoft/TypeScript#36967
@tadhgmister
Copy link

For people who are using something like redux-saga and looking for a work around, I want to make my work around here easier to find (particularly for myself 😄

@DanielRosenwasser DanielRosenwasser added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript and removed Needs Investigation This issue needs a team member to investigate its status. labels Sep 22, 2020
@DanielRosenwasser DanielRosenwasser removed this from the Backlog milestone Sep 22, 2020
@Azarattum
Copy link

I think this suggestion would be extremely useful and powerful. @tomwidmer, with the suggested implementation overloads are already possible! Check out this code:

/** For demonstration purposes */
const uniqueSymbol = Symbol();
type CallExpression<A extends any[], R> = (...args: A) => R;

/** Yield type annotation */
type Yield = <T>(p: T) => 
    T extends Promise<infer U> ? U :
    T extends CallExpression<any, infer U> ? U :
    T extends (infer U)[] ? U :
    T extends typeof uniqueSymbol ? 42 :
    unknown;

/** Mock yield expression */
const y: Yield = (() => {}) as any;

/** Type checks */
let a = y(Promise.resolve(42)); //number
let b = y(() => 42);            //number
let c = y([42]);                //number
let d = y(uniqueSymbol);        //42
let e = y(42);                  //unknown

This feature would be extremely useful for me personally if implemented. I want to use it in my own code (not with an external library) to write cancelable async workflows. Unfortunately await does not support an external abort signal, so we have to use generators with a runner (check out this amazing blog post). The only thing missing right now is the type checking. Implementing this feature request would 100% solve my use case!

@scorbiclife
Copy link

scorbiclife commented Sep 9, 2022

Apologies if I'm missing something basic, but would something like this be possible to infer from TypeScript?

https://www.typescriptlang.org/play?#code/PTAEBUAsEsGdQCYHsCm8B2SAuoDGT0sAnAQwWlxy0hVCwE8AHW2e2LFAWwBpRIlmAMwCuAG1H0A-AChp0QiiKCSuWgFkCZCgHEU6RSSxIiAHgCa0FKIQBhJMMKgUADw7oEGYZwBGi3hasEABk4HBc3D1AHAGtMAHd0AG0AXV4AJRQsYSJ0J1c9SJJ0el4AOXCAQSIAc1gQ9jyI+Bj4pOSUgD5QAG9pUH7QfVcTAEl3F0aC+ADrOwcsDoAKADpVkhrYAC5Qctcq2vqsRLGEF2SASm2RjlIjIgysnIzYMSwTB+z0DoBuPoGht4nCbhKaDLy+IigAC8YJ8iiWq2W61q212WH2dVCx3GzguVxuhmMMwQz1e5ks1kO2NOuJ+0gAvrJ8OgGtU9AAxBy4aGgEToSjQAgAKlAi3OPT+-WZDRIPPoFIQgxQcVAAClhOxFgBWc6-AZ4AgNbxyhVKlXqzUAIgAbJbdZLQERMp8zWqNVhFrKANSgRjrWAoMYe7y8ACMAAZzvb6aASPAxdCuhp0FpcLp9LdjCYAEy8RJqEj0XwmdDg+G8AtFlAmdhEeTVDqpUCV4uluFEDp5xIOU6CeQoBBNxJtiFD2v15LJDqyPkCgiDEjQABuKAAIkhSthDIL0CZwOHeOBQ+kuosHWz0Jz+dsE1Ck5pyGm9AY7jm8y3q-vO83C8Wj42K1-as0m-RJu3GPt9EHPN9yHI9J2ncVen1aUcAvHkLyvXAxT1AZUJ6UAlxIURhBQbZOFlGMYQvZYARMcMlh7FBIIHe19SdR5cgo5ZvHkBBFk9cU7wlfUUMNHBukI4jSPI40qNAGi6NDJYSDY0T+g4l0fB4viBO8ISumQ9TRPwySiJIsjHTQV5QHkxTwhzJZ9Nw4yBk0nIrJeUQsBc0T6XOB1-IZWQQB-U5Y1ATg0HgYh5XQao6CQBTn1IURoAAL1oah7GqSAQrAdkiCQTg+CwLBGC2EBcGyQVYGgBgAFoEDrFd0GWYxqmATgH1gBr5AagArEgiNgXA60YLAAGJkHy5sH1kBhmDmlMED3LoYSM-pePcEwAFUlkEG8sG2cADOWsg9o6S5ztW-bgukRb1AfMYM3AJhqzUcBJnceBkwuop6A6dbmy+kEfpukx5EERQIC6SQIFAbZ9BXIhflmj8mVEON4AtN5wC6aBOEYUQuD0LBfofNaRIGcyZIgNGxJZYhhEoYxFlpyzTup9TqDgZYOZ5DnfMZB1ttug6bw5k6zr+8Xrtly7udE9zckERZedgfnpJQNT+kZfUHSMABlYh6wTTbledDyAANccWAASboNa1iz-Ot4X7vwzdefinkLbFy7FkO0UpaVFGZcp-b5cjwyHXYq3cg14XuFkfVjdN+LzbjtyE9AS1vZgeLLWT+7Hp-KsqZhXGqYAHzod6kEEHZsEL6pZAe97QDGMbScIUZvsiEd4R5B1EmPUBc1AABmXgABZeC1XhrV4AB2XgAA5eAATjDA9QFDcfQ0n0MZ-3+f98X-fl-3tf983-ed4nvfs3H7NJ+zU-s3P7NL8RIeiBSO0EYyRfhl1XCgHuUU+4jAHp4ds61R4NXHnvV+vBT7n0vtfW+99H4RjDIfY+p9Qzn1DJfUM19Qy31DPfUMj9szP1fu-T+58-5lgAZOY4ICFqdwLIwBWoN8jgwBp0P2oBEjQFAPIUA0QUD0EbhAZI2x+HiKnIyMuvDwAbkqBsPcsDYzFBERtMREipEyLkU3cAijJF6P-qAeGiRwCJHAZAsmkNGzJERvo+gtkZxciwDuEUyBvbbgICYB0xI5iODBoPNhKd9QADVtaHD0S0JACQUhxIGB8HIb1mDSFPJhLkN4I4rR0ClQkphwkKkiVgTJ-ReEK0SRZQ4nYHQK2yegXJKBWn6g0VovYOimmkRafkpCDp8LoWohyLkOEHSzn8fOdgKBGD92iXAiESxQ5DJQFSZxTooGAkbNHFaJhtlUmAbHVy+EnReRwFMtqdERibO1rrUS0Am6LBua8ZYyB9BjNcvHTinlvlC2znrMFVkgVfO8i7UiOkdpnKxN3fZrinlTgEvIDM2yzpLJWci3uhyliYsUNi15+sc5AtxfRRiEF+wIGjEycSwLvI8iCVuBZ6BFiFP5PaIAA

Edit: Improved the code in #32523 (comment)

@tadhgmister
Copy link

@nightlyherb I think the only 'basic' thing you are missing is that in general the number of yield sites is not always known:

interface A {a:string}
interface B {b:string}
function* example(cond1:boolean, cond2:boolean){
  if(cond1){
    const x: A = yield 1
  }
  if(cond2){
    const y: B = yield 2
  }
  return "hi there"
}

in this case you very much cannot possibly make a MonadicGenerator as long as cond1 and cond2 are left as type boolean but this is a valid way to write code for libraries that this conversation is applicable for.

What you have written does accomplish exactly what you are describing, the reason it hasn't attracted more attention is that it doesn't serve the general purpose case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

9 participants