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);
And `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 `call`ed function in place of `getANumber`.
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](https://www.typescriptlang.org/docs/handbook/advanced-types.html#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> >;
The generator function you yield to can be typed with a narrow return type eliminating the need for any casting between yield and assignment.
Comments are closed.