Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript: Use types on call() from redux-saga

How can I set the types of a function using call()?

I have this function:

export function apiFetch<T>(url: string): Promise<T> {
    return fetch(url).then(response => 
        {
            if (!response.ok) throw new Error(response.statusText)
            return response.json().then(data => data as T);
        }
    )  
}

This function can be used like:

let resp = await apiFetch<ServerResponse>("http://localhost:51317/Task");

By using the function as you can see in the above piece of code, resp is correctly string-typed. So intellisense offers me all the attributes of the ServerResponse interface.

However, this function has to be call inside a worker from redux-saga which does not allow, async functions:

function* refreshTaskSaga():any {
    yield takeEvery("TASK_REFRESH", workerRefreshTaskSaga);
}


function* workerRefreshTaskSaga() {
  //I need to call the function here
}

I try to call it using yield + call, as redux-saga documentation said:

a) let resp = yield call(apiFetch, "http://localhost:51317/Task");
b) let resp = yield call(apiFetch<ServerResponse>, "http://localhost:51317/Task");

The first option, execute the function as expected, however resp has any type. The second options throws me an exception.

No overload matches this call.
  The last overload gave the following error.
    Argument of type 'boolean' is not assignable to parameter of type '{ context: unknown; fn: (this: unknown, ...args: any[]) => any; }'.ts(2769)
effects.d.ts(499, 17): The last overload is declared here.

Any idea of the correct syntax to call it and don't lose types?

like image 797
Rumpelstinsk Avatar asked Oct 23 '19 11:10

Rumpelstinsk


1 Answers

Unfortunately, the left side of a yield always has type any. This is because a generator function can in principle be resumed with any value. Redux saga behaves in a predictable way when running generators, but there's nothing stopping someone from writing other code that steps through your saga and gives you values that are unrelated to what you yielded, as in:

const iterator = workerRefreshTaskSaga();
iterator.next();
// You might have been expecting a ServerResponse, but too bad, you're getting a string.
iterator.next('hamburger'); 

Only if you can assume that redux saga is running your generator can you make predictions about the types, and typescript doesn't have a way to say "assume this generator will be run by redux saga (and all the implications that includes)".

So you'll need to add the types yourself. For example:

const resp: ServerResponse = yield call(apiFetch, 'url');

This does mean you are responsible for getting the types correct. Since typescript can only tell that it's an any, it will trust you with whatever you say the type is. So typescript can verify that the code following this interacts correctly with a ServerResponse, but if it's not actually a ServerResponse, typescript can't point that out to you.

One thing i often do to get a bit more typesafety is use ReturnType, as in:

const output: ReturnType<typeof someFunction> = yield call(someFunction);

It's still up to me to know that ReturnType<typeof someFunction> is correct, but assuming i did that, then if someone changes the implementation of someFunction to cause it to return something different, output's type will be updated to match.

like image 59
Nicholas Tower Avatar answered Sep 22 '22 08:09

Nicholas Tower