agiledigital / typed-redux-saga

An attempt to bring better TypeScript typing to redux-saga.
MIT License
315 stars 33 forks source link

Propagate return type of call effect with generic arguments #691

Open sebc-vm opened 1 year ago

sebc-vm commented 1 year ago

I expect the result type of a call effect with generic arguments to be propagated.

However, this does not work:

function* wrappedCall<Args extends any[], RT>(
    saga: (...args: Args) => SagaIterator<RT>,
    ...args: Args
): SagaIterator<RT> {
  return yield* call(saga, ...args); // Type 'SagaReturnType<(...args: Args) => SagaIterator<RT>>' is not assignable to type 'RT'.
}

Changing the type of the arguments to any[] allows the result type to propagate:

function* wrappedCall<RT>(
    saga: (...args: any[]) => SagaIterator<RT>,
    ...args: any[]
): SagaIterator<RT> {
  return yield* call(saga, ...args); // Result type is RT as expected
}

The problem comes from the definition of SagaReturnType:

export type SagaReturnType<S extends Function> =
  S extends (...args: any[]) => SagaIterator<infer RT> ? RT :
  S extends (...args: any[]) => Promise<infer RT> ? RT :
  S extends (...args: any[]) => infer RT ? RT :
  never;

When S is the type of a function whose arguments are a constrained type variable, typescript cannot compare any[] against the type variables and is unable to decide the conditional types.

The following code illustrates this limitation in the type system:

type IsVoid<T extends void> = T extends void ? true : false;

function foo<U extends void>(): [IsVoid<void>, IsVoid<U>, IsVoid<U>] {
  return [true, true, false];
}

Parameterizing SagaReturnType over Args fixes the problem by avoiding the comparison between any[] and a constrained type variable. The following declarations fix the initial wrapped call example above:

import {SagaIterator} from "redux-saga";
import {CallEffect} from "redux-saga/effects";

declare module 'typed-redux-saga' {

  export type SagaReturnType<S extends Function, Args extends any[]> =
    S extends (...args: Args) => SagaIterator<infer RT> ? RT :
    S extends (...args: Args) => Promise<infer RT> ? RT :
    S extends (...args: Args) => infer RT ? RT :
    never;

  export function call<Args extends any[], Fn extends (...args: Args) => any>(
    fn: Fn,
    ...args: Args
  ): SagaGenerator<SagaReturnType<Fn, Args>, CallEffect<SagaReturnType<Fn, Args>>>;

}

This pull request copies and adapts the definition of SagaReturnType from @redux-saga/core as outlined above and makes the necessary changes throughout the type definitions.