Search code examples
typescriptgeneratorredux-saga

How to type multiple yield generator (redux-saga)


our redux-saga generator contains multiple yield statements, that return different results. I need help typing them properly.

Here's an example:

const addBusiness = function* addBusiness(action: AddBusinessActionReturnType): Generator<
  Promise<Business> | SelectEffect | PutEffect<{ payload?: ActionPayload<Array<Business>>; type: string }>,
  void,
  Business | BusinessesContainer
> {
    const { url, showToast = true } = action.payload;

    const businessDetails: Business = yield Network.get<Business>( // ts error: Type 'Business | BusinessesContainer' is not assignable to type 'Business'.

      `businesses?url=${url}`,
    );

    if (showToast) {
      const getBusinessesFromState = (state: AppState) => ({
        ...state.business.businesses,
      });
      const businesses: BusinessesContainer = yield select(getBusinessesFromState); // ts error: Type 'Business | BusinessesContainer' is not assignable to type 'BusinessesContainer'
      onAddBusinessSuccessToast(businesses, businessDetails);
    }

    yield put({ // ts error: Type 'SimpleEffect<"PUT", PutEffectDescriptor<{ type: string; payload: Business[]; }>>' is not assignable to type 'SelectEffect'
      type: constants.SAVE_BUSINESS_REQUEST,
      payload: [businessDetails],
    });

In the comments above you see the ts errors that we get. Any help would be appreciated. Thanks


Solution

  • It seems like you are definitely guilty of some over-typing here. Certainly yield Network.get<Business>() returns a Business, right? You shouldn't have to write const businessDetails: Business if the value that you are assigning to it is a Business.

    These generator return types can be really hard to type properly, but Typescript is smart enough to figure out the proper type. You could leave it off entirely and it would be fine. If you've having a hard time figuring out the correct type you can delete the return type temporarily to see what the inferred type is. That's how I resolved your first issue, which is that that the union Business | BusinessesContainer needs to be changed to an intersection Business & BusinessesContainer.

    I don't know what the ActionPayload type is but it seems unnecessary. You are typing the payload of your put action as ActionPayload<Array<Business>>, but Array<Business> is correct on its own. There's no reason for the payload to be optional when you are always providing it.

    Those two changes solve your typescript issues...for now. I usually see API calls executed inside a call effect, which would make the types even more complex. But I'm not sure if the call is actually required.

    const addBusiness = function* addBusiness(action: AddBusinessActionReturnType): Generator<
      Promise<Business> | SelectEffect | PutEffect<{ payload: Array<Business>; type: string }>,
      void,
      Business & BusinessesContainer
    > {
        const { url, showToast = true } = action.payload;
    
        const businessDetails: Business = yield Network.get<Business>( `businesses?url=${url}` );
    
        if (showToast) {
          const getBusinessesFromState = (state: AppState) => ({
            ...state.business.businesses,
          });
          const businesses: BusinessesContainer = yield select(getBusinessesFromState);
          onAddBusinessSuccessToast(businesses, businessDetails);
        }
    
        yield put({
          type: "SAVE_BUSINESS_REQUEST",
          payload: [businessDetails],
        });
      }
    
    // dummy types that I filled in to check types
    
    type Business = {
      name: string;
      id: number;
    }
    
    type BusinessesContainer = {
      businesses: Business[];
    }
    
    const Network = {
      get: async <T>(url: string): Promise<T> => {
        return {} as unknown as T;
      }
    }
    
    type AddBusinessActionReturnType = {
      type: string;
      payload: {
        showToast?: boolean;
        url: string;
      }
    }
    
    type AppState = {
      business: {
        businesses: BusinessesContainer;
      }
    }
    
    const onAddBusinessSuccessToast = (businesses: BusinessesContainer, businessDetails: Business) => undefined;