Search code examples
reactjstypescriptredux-saga

redux-saga "call" effect not typing saga params correctly


I have a saga defined as the following:

type GenericFunction = (...args: any[]) => any;

interface IFetchSaga<T extends GenericFunction> {
  saga: T,
  args: Parameters<T>
}

function* triggerChange<T extends GenericFunction>(fetchSaga: IFetchSaga<T>, shouldFetch: boolean) {
  // ...do stuff
  if(shouldFetch) {
    yield call(fetchSaga.saga, ...fetchSaga.args);
  }
  // ...do stuff
}

When I try to use this saga inside another one, using the call effect I am not getting the correct typing for the IFetchSaga args:

function* fetchData(id: number, date: string) {
  // ...do fetch
}

function* load() {
  // ...do stuff
  yield call(triggerChange, {
      saga: fetchData,
      args: ['10', '2021-03-22'] // I don't get an error saying that '10' should be a number
    },
    true
  );
}

For instance, when I try to execute it directly (as if it was any other function), the typing works correctly:

triggerChange({
  saga: fetchData,
  args: ['10', '2021-03-22'] // Here I get the error saying that '10' should be a number
});

Is there anything I can do to get the args typing correctly when calling my saga?


Ps.: I know I can define my fetchSaga param outside of the call effect, but then I would have to always call the IFetchSaga with a typeof (and I would like to avoid it):

function* fetchData(id: number, date: string) {
  // ...do fetch
}

function* load() {
  // ...do stuff
  // This would work, but I want to avoid having to do it 😬
  const fetchSaga: IFetchSaga<typeof fetchData> = {
    saga: fetchData,
    args: ['10', '2021-03-22'] // Get the typing error for '10'
  };

  yield call(triggerChange, fetchSaga, true);
}


Solution

  • Solution

    My suggestion to fix this is to create the IFetchSaga<T> object through a helper function.

    function fetchable<T extends GenericFunction>( saga: T, ...args: Parameters<T>): IFetchSaga<T> {
      return {saga, args};
    }
    

    You could pass the args as an array if you want to.

    This allows typescript to easily infer the proper args for the function and make sure that they match.

    function* load() {
      // error: Argument of type 'string' is not assignable to parameter of type 'number'
      yield call(triggerChange, fetchable(fetchData, "10", "2021-03-22"), true);
      // okay :)
      yield call(triggerChange, fetchable(fetchData, 10, "2021-03-22"), true);
    }
    

    Problem

    The reason that the original version fails is similar to this answer where I go more in depth. triggerChange is a generic function that can be called with any T. When we are creating a call effect for triggerChange it does not need to have any particular T type since the T is determined at the time when the function is called. The type for the arguments falls back to just GenericFunction where args are any[], so there is no issue with assigning your array to that.

    Our solution works because we are inferring a specific T by directly calling fetchable. It's the fetchable function that gives us errors -- not the call. If we were to declare that the fetchable T is just GenericFunction then we would have the same issue as before.

    // no errors, even though we want them
    yield call(triggerChange, fetchable<GenericFunction>(fetchData, "10", "2021-03-22"), true);