Typescript does not infer promise type when using yield in a generator function
Asked Answered
N

1

6

I'm using Axios to handle some API fetching, and I'm executing that call within a generator; async/await is not an option for this particular project. For some reason, even though axios is getting my types right, typescript is inferring an any type when I use the yield keyword.

function* foo() {
  // axios.get is returning Promise<AxiosResponse<T>>
  // but data is being inferred as "any"
  const { data } = yield axios.get<T>(url); 
  
  // do some stuff with data
}

If I specfically type the axios response, it works fine, but I feel like I'm missing something, since TS isn't getting the type automatically

function* foo() {
  const { data }: AxiosResponse<T> = yield axios.get<T>(url); 
  
  // do some stuff with data
}

Is there some other step or config I'm missing?

Here's my tsconfig

{
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/",
    "sourceMap": true,
    "noImplicitAny": false,
    "module": "commonjs",
    "target": "es6",
    "jsx": "react",
    "removeComments": true,
    "allowJs": true,
    "skipLibCheck": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "rootDirs": ["src", "src/stories"],
    "paths": {
      "*": ["./src/custom-types/*"]
    },
    "types": ["@emotion/core", "jest"]
  },
  "include": ["./src/**/*"]
}

Nee answered 30/8, 2021 at 13:36 Comment(6)
@ikhvjs - "async/await is not an option for this particular project."Acropolis
Can you show us the call to this function?Acropolis
How is async/await not an option? Especially since you are using the typescript compiler, and are fine with generator functions?Benignity
@Benignity the library I'm using (mobx-state-tree) has a preferred method for handling async actions, and recommends against async/await.Nee
This answer might give some inspiration as well.Weems
@KevinWhitaker - FWIW, see this article: coolsoftware.dev/blog/write-async-await-with-mobx-state-tree by one of the mobx-state-tree contributors about using async/await with it.Acropolis
A
5

Preface: You've said you can't use async/await because you're using mobx-state-tree, which says you can't use it on protected trees. But see this article on how to use async/await successfully with it. (Basically, instead of directly updating things in your async function, you call an action to do it.) It's on the blog of Tyler Williams, one of the contributors to mobx-state-tree. I found it in March 2024; no idea when it was written, it's undated ☹️ and not in archive.org. He says they're working on updating the mobx-state-tree documentation.

Answering the generator question:

TypeScript can't infer the type of the yield operation, because that's controlled by the calling code. In your case, it sounds like this generator is being used by code that handles the promise from axios and responds by providing the axios result in the call to g.next. That makes sense if you're in an environment where you can't use async/await; generators can be used to allow async logic to flow more clearly, where the generator is driven by a helper that gets the promise from the generator, waits for it to settle, then passes the fulfillment value back to the generator via next — yield largely takes the role of await when doing that. (co is one example of a library that enables using generators this way.) So the code using the generator expects the generator to yield a Promise and it gives back the promise's fulfillment value. In this case, that would be yielding a Promise<AxiosResponse<T>> and getting back an AxiosResponse<T>.

To handle that, you need to annotate the function using the Generator type, which accepts three type parameters:

  • T - the type of what the generator produces via yield.
  • TReturn - the type of what the generator returns when done.
  • TNext - the type of what the generator consumes (receives from yield).

So applying that to your example, we'd add a generic type parameter to foo and annotate it with Generator:

function* foo<T>(): Generator<Promise<AxiosResponse<T>>, ReturnType, AxiosResponse<T>> {
    const { data } = yield axios.get<T>(url); 
    
    // ...do some stuff with data...

    return /*...something of `ReturnType` (or use `void` as the type argument above)... */;
}

Just for anyone who isn't as familiar with yield and generators as they might like, here's an example where the generator produces strings, returns a boolean, and consumes numbers (playground link):

function* example(): Generator<string, boolean, number> {
    const a = yield "a";
    console.log(`Received ${a} for a`);
    const b = yield "b";
    console.log(`Received ${b} for b`);
    const c = yield "c";
    console.log(`Received ${c} for c`);
    const flag = (a + b) > c;
    return flag;
}

const g = example();
let result = g.next();
console.log(result.value);  // "a"
result = g.next(1);         // Passes 1 to the generator
// (Generator logs "Received 1 for a")
console.log(result.value);  // "b"
result = g.next(2);         // Passes 2 to the generator
// (Generator logs "Received 2 for b")
console.log(result.value);  // "c"
result = g.next(3);         // Passes 3 to the generator
// (Generator logs "Received 3 for c")
console.log(result.value);  // false (1 + 2 > 3 is false)
Acropolis answered 30/8, 2021 at 16:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.