There’s currently no way to even model Saga’s behavior into TypeScript, so any effect you
yield produces an
You can get back into the realm of type safety with some manual type annotations, but these are often a pain to write and maintain. After a few rounds of iteration, we finally have a solution that works well in our codebase to make these manual annotations easier. We can now write:
const aNumber: CallReturnType<typeof getANumber> = yield call(getANumber);
aNumber will be properly inferred as the right type, regardless of whether
getANumber simply returns a number directly, returns a promise, or is a saga that returns a number. Your own
call effects work the same way; just use the name of the
called function in place of
Before this, we had an ad hoc approach to manually extracting the type and duplicating it in the definition of the variable. If the function/saga we were calling changed, our types would get out of alignment with the actual behavior, and we’d have subtle type errors that were worse than having no type information at all. At least with
any, we know we’re riding without a seatbelt.
I hope this helps your sagas get more type safe and convenient to write and maintain!
CallReturnType works by mirroring the runtime behavior of
call into the type by leveraging conditional types to effectively do decision-making in the type checker (similar to what
call does at runtime). It looks at the return type of the provided function type and does its best to produce an answer consistent with the runtime behavior. The
typeof operator lets us reference any function in the type to get its type information to power the machinery.
/** Strip any saga effects from a type; this is typically useful to get the return type of a saga. */ type StripEffects<T> = T extends IterableIterator<infer E> ? E extends Effect | SimpleEffect<any, any> ? never : E : never; /** Unwrap the type to be consistent with the runtime behavior of a call. */ type DecideReturn<T> = T extends Promise<infer R> ? R // If it's a promise, return the promised type. : T extends IterableIterator<any> ? StripEffects<T> // If it's a generator, strip any effects to get the return type. : T; // Otherwise, it's a normal function and the return type is unaffected. /** Determine the return type of yielding a call effect to the provided function. * * Usage: `const foo: CallReturnType<typeof func> = yield call(func, ...)` */ export type CallReturnType<T extends (...args: any) => any> = DecideReturn< ReturnType<T> >; /** Get the return type of a saga, stripped of any effects the saga might yield, which will be handled by Saga. */ export type SagaReturnType<T extends (...args: any) => any> = StripEffects< ReturnType<T> >;