Search code examples
reactjstypescriptreduxredux-saga

Typing generator functions in Redux-Saga with Typescript


Recently I've started refactoring my React project to start using Typescript in it. I've encountered a problem with typing which I don't know how to overcome.

Tech-stack: Typescript, React, Redux, Redux-Saga

I'm also using ESLint with AirBnB ruleset.

I'm trying to write a saga which uses async function defined in my authorization service. The code below is my 2nd attempt to write this. I've splitted the signIn saga in order not to mix the yield return types as suggested on the end of this article.

export function* callSignInService(userCredentials: UserCredentialsInterface): 
    Generator<CallEffect, UserAuthDataInterface, UserAuthDataInterface> > 
{
    return yield call(signInService, userCredentials);
}

export function* signIn(action: PayloadAction<UserCredentialsInterface>): 
    Generator<StrictEffect, void, PutEffect> 
{
    try {
        const { payload } = action;
        const userData = yield* callSignInService(payload);

        yield put(signInSuccess(userData));
    } catch (error) {
        yield put(signInFailure(error.message));
    }
}

A simplified version of the signIn service:

const signIn = async ( userCredentials: UserCredentialsInterface ): Promise<UserAuthDataInterface>
{
    /* ... Some credential format validations ... */
    try {
        const response = await axios.post('someUrl', userCredentials);
        return {
            /* objectContent */
        }: UserAuthDataInterface
    } catch (error) {
        throw new Error(AuthorizationErrorType.SERVER_ERROR);
    }    
}

The problem is I'm still getting an error on:

const userData = yield* callSignInService(payload);

saying :

TS2766: Cannot delegate iteration to value because the 'next' method of its iterator expects type 'UserAuthDataInterface', but the containing generator will always send 'PutEffect'.   Type 'SimpleEffect<"PUT", PutEffectDescriptor>' is missing the following properties from type 'UserAuthDataInterface': user, token

I understand that the issue is that my signIn generator NextType is set to PutEffect and it expects expression yield signInService(userCredentials) to return PutEffect but that's not my goal. I want to use the signInService to get UserAuthData and then put these data into signInSuccess action (signInSuccess is created using action creator).

I've read several resources on how to properly type the generators but still can't get it to work with my example. I found information that redux-saga has or had problems regarding the typing but I am not sure if it's still the case in 2021.

Am I missing some obvious things here? Maybe there is a problem with my attempt to write such Saga. I would be pleased if someone could take a look and give me an advice how to handle this problem.


Solution

  • The first generic type parameter is the type of the yielded values in the saga. It's the "steps", in other words.

    Moving callSignInService to a separate function doesn't actually help with the types because we are still yielding that result.

    Your signIn saga has two steps with distinct types -- calling the API and dispatching an action. The generic needs to be a union of the two types.

    export function* signIn(
      action: PayloadAction<UserCredentialsInterface>
    ): Generator<
      // step types
      CallEffect<UserAuthDataInterface> | PutEffect<AnyAction>,
      // return type
      void,
      // intermediate argument
      UserAuthDataInterface
    > {
      try {
        const { payload } = action;
        const userData: UserAuthDataInterface = yield call(signInService, payload);
    
        yield put(signInSuccess(userData));
      } catch (error) {
        yield put(signInFailure(error.message));
      }
    }
    

    I'm assuming that your settings require an return type declaration (or else this would be easy!). It can be really helpful to remove the declaration temporarily to see what Typescript infers. That's how I got this solution.