TypeScript: Use types on call() from redux-saga
Asked Answered
M

2

17

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?

Marsupium answered 23/10, 2019 at 11:58 Comment(2)
Where is call coming from ?Undertaking
My mistake. I'm going to update the question. Call comes from redux-sagaMarsupium
B
23

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.

Bowls answered 23/10, 2019 at 19:23 Comment(5)
I understand that TS doesn't currently provide the ability to type what comes back from yield. Do you think it's possible to add this to the language? The implicit contracts between the generator and the thing calling 'next' could be made explicit.Florinda
Looks like TS v3.6 improved the type ability for Generator.Madelaine
I would have thought I could cast this myself, and using the ReturnType felt like as could as I could get. But even using the same code described here, I'm still getting an error. let response: ReturnType<typeof apiFunction> = yield call(apiFunction, action.payload, action.meta); Type 'unknown' is not assignable to type 'Promise<IRequestResponse<ResponsePayload>>'Abbatial
Just wanted to point a possible error in this answer, if someFunction is a function (not a type) it should be typed like this: ReturnType<typeof someFunction> instead of ReturnType<someFunction>Mata
Still have Type 'unknown' is not assignable to type 'ServerResponse' error. Which is removed by adding a return type of the generator function in which the yield takes place, like said "the left side of a yield always has type any" : Generator<YieldRightSideType, void, any>.Linville
P
2

reading: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-6.html, I realized that we can set the yield type as the third argument in Generator Type

import { AnyAction } from "redux";
import { call, put, fork, takeLatest, StrictEffect } from "redux-saga/effects";
import { apiRequest } from "api/requests";
import { setAuthenticationLoader, setLoginError, setToken } from "./actions";
import { sagaTypes } from "./types";
import { LoginResponse } from "api/requests/authentication";

export function* requestLogin(
  action: AnyAction
): Generator<StrictEffect, any, LoginResponse> {
  const setError = (err?: any) => put(setLoginError(err));
  yield put(setAuthenticationLoader(true));
  yield setError();
  try {
    const data = yield call(apiRequest.authentication.login, action.payload);
    if (!data.token) setError(data);
    else yield put(setToken(data.token));
  } catch (err) {
    yield setError(err);
  } finally {
    yield put(setAuthenticationLoader(false));
  }
}

function* watchLoginRequest() {
  yield takeLatest(sagaTypes.REQUEST_LOGIN, requestLogin);
}

export const authenticationSagas = [fork(watchLoginRequest)];
Permissible answered 21/1, 2022 at 18:14 Comment(2)
This isn't a generic solution to the problem, because not every yield must have the same type. Here data is correctly typed, but what if you have another call or select in the same saga.Dichotomy
A union can then be used, but this is also nice perfect.Dichotomy

© 2022 - 2024 — McMap. All rights reserved.