Navigate back to the homepage

Redux-saga and Typescript, doing it right.

Fabien Trestour
February 19th, 2020 · 3 min read

Redux-saga is a widely used library that helps to deal with side effects in a front-end app. Typing sagas properly is not obvious though. Here’s a guide on how to do it!

What are sagas

Sagas are functions that return generators:

1function* hello(foo: string) {}
2// -> (foo: string) => Generator

Even if redux-saga relies on other concepts (like effects), typing a saga mostly boils down to typing a generator.

Typing a generator

Generators typing was improved in TS 3.6. The current Generator type has three type parameters. Some examples will help you in finding out what they describe.

Inferred type for a simple generator factory

1function* thisIsAGenerator(foo: boolean) {
2 yield "hello world";
3 return 0;
4}
5// -> Generator<string, number, unknown>

This example allows us to understand what the first two type arguments stand for in a generator type:

  • The first one is the type of the values we yield. We’ll call it YieldType
  • The second one is the type of the generator’s return value. We’ll call it ReturnType

If values of different types are yielded, YieldType is the union of each possible type

1function* thisIsYetAnotherGenerator(foo: string) {
2 yield "hello world";
3 yield ["foo", "bar"];
4 return 0;
5}
6// -> Generator<"hello world" | string[], number, unknown>

Where inferred generator types fall short

Consider this generator:

1function* thisOneIsABitDifferent(foo: string) {
2 const bar = yield "baz";
3 console.log(bar);
4 return 0;
5}
6// -> Generator<string, number, unknown>

Inspecting the type of the bar value turns out to be quite weird. Indeed, it is of type any when it could be expected to be of type string.

Having any values pop somewhere in TS code is really a pain since it limits a lot the help provided by typing later in the function. Why can’t TS infer the type of bar automatically?

To understand it, it is necessary to dig a bit deeper in generator behaviour.

Let’s consider this code from the TS 3.6 release blogpost:

1function* counter() {
2 let i = 0;
3 while (true) {
4 const stop = yield i++;
5 if (stop) {
6 break;
7 }
8 }
9 return "done!";
10}

What is the type of stop here? If your answer is number, you’re wrong.

Actually, stop could a boolean, a string, an object. From all we know according to this code, it could be of any type. It depends on how the generator is used.

The yield keyword

Let’s refer to yield’s documentation:

The yield keyword is used to pause and resume a generator function.

The yielded value defines the new current value of the generator. It can be seen in the example they provide:

1function* foo(index) {
2 while (index < 2) {
3 yield index++;
4 }
5}
6
7const iterator = foo(0);
8
9console.log(iterator.next().value);
10// expected output: 0
11
12console.log(iterator.next().value);
13// expected output: 1

What is the stored value when the result of the yield operator is saved then? The saved value actually is the argument provided to the iterator.next function.

Using the previously defined counter generator:

1const iter = counter();
2let curr = iter.next();
3while (!curr.done) {
4 console.log(curr.value);
5 curr = iter.next(curr.value === 5);
6}
7console.log(curr.value.toUpperCase());
8
9// prints:
10//
11// 0
12// 1
13// 2
14// 3
15// 4
16// 5
17// done!

Here, next is called with a boolean value (curr.value === 5). Which means stop in the definition of counter is of type boolean.

Knowing how a generator is used is necessary to know what type yield operations return.

The full typing of a generator

It is now possible to fully type a generator:

1function* counter(): Generator<number, "done!", boolean>;
  • counter successive values are numbers
  • counter returns "done!" when there are no new values to generate
  • counter takes boolean values as an argument to generate new values

This example will not compile since there’s a conflict on the type of bar:

1function* thisIsAWrongSaga(foo: string): Generator<string, number, boolean> {
2 const bar: string = yield "baz";
3 // Type 'boolean' is not assignable to type 'string'.
4 console.log(bar);
5 return 0;
6}

Typing a saga

Everything up to this point works for any kind of generator factory. However, redux-saga is not limited to writing generator factories. The library does all the heavy lifting related to orchestrating the sagas (i.e. running effects and calling next).

Using StrictEffect

Sagas introduce the concept of effects. Here is the inferred type for a saga that uses the delay effect:

1function* thisIsASaga(foo: string) {
2 yield delay(1000);
3 return 0;
4}
5// -> Generator<SimpleEffect<"CALL", CallEffectDescriptor<true>>, number, unknown>

Here’s a quick look at @types/redux-saga’s definition of a SimpleEffect:

1export interface Effect<T = any, P = any> {
2 "@@redux-saga/IO": true;
3 combinator: boolean;
4 type: T;
5 payload: P;
6}
7
8export interface SimpleEffect<T, P = any> extends Effect<T, P> {
9 combinator: false;
10}

redux-saga basically yields simple objects describing what actions should be done without actually doing them.

That’s why sagas are supposed to be easily testable. Limiting saga yields to effects (including call effects) allows to test values that do not depend on the implementation of external functions.

@types/redux-saga provides a StrictEffect type that can be used in generator types to allow nothing but effects in yields:

1const wiiiiiiiiii = () => "hihihihihi";
2function* thisIsAWrongSaga(foo: string): Generator<StrictEffect, number, any> {
3 yield wiiiiiiiiii();
4 // Type 'string' is not assignable to type 'StrictEffect<any, any>'.
5 return 0;
6}

Handling forks

Some effects are supposed to return specific values. See fork’s documentation for instance:

The result of yield fork(fn …args) is a Task object. An object with some useful methods and properties.

Knowing how to type a generator, this is now easy to handle:

1function* thisIsASagaThatForks(
2 foo: string
3): Generator<StrictEffect, number, Task> {
4 const potato = yield fork(function*() {
5 yield "Bepis";
6 });
7 cancel(potato);
8
9 return 0;
10}

Limitation of this typing process

This way of typing comes with a few constraints. One of them is the handling of different yield return types.

1const createPotato = () => "potato";
2const createTomato = () => false;
3
4export function* thisYieldsManyStuff(): Generator<
5 StrictEffect,
6 string,
7 string | boolean
8> {
9 const potato = yield call(createPotato);
10 const tomato = yield call(createTomato);
11 return "yay!";
12}

potato and tomato will both have the same type which is string | boolean .

Splitting sagas in smaller bits should allow to overcome this issue in most cases.

The yield* operator

This is yield*’s defintion:

The yield* expression is used to delegate to another generator or iterable object.

This operator allows to chain generators. An added bonus is that it provides a good typing of the result value:

1function* secondaryGenerator() {
2 yield delay(1000);
3 return true;
4}
5// -> Generator<SimpleEffect<"CALL", CallEffectDescriptor<true>>, boolean, unknown>
6
7function* mainGenerator(): Generator<StrictEffect, void, string> {
8 const foo = yield call(wiiiiiiiiii); // string
9 const secondaryGeneratorResult = yield* secondaryGenerator(); // boolean
10 console.log(secondaryGeneratorResult);
11}

Combining this with proper saga splitting should allow you to get a perfect typing every single time!

Practice!

Here is a small typescript sandbox with some of the examples given in this article. Playing with it may help better understanding how all of this works. As with a lot of things, practice makes perfect!

More articles from Lalilo

The learning mindset

When you join Lalilo, it means we, the people you saw during your interviews, trust you. We have no doubt about your capabilities and skills. We have no doubt you will be a huge asset to Lalilo and that Lalilo will make you grow up.

November 13th, 2019 · 2 min read

Capturing visuals bugs before they reach production

At Lalilo, we try to reuse React components throughout the apps which means a Button might be used a dozen times on different pages.The style of our components are layered, which means the style for a given button might be defined in Button, then in BigButton and finally in BigBlueButton.

February 28th, 2019 · 4 min read
© 2018–2020 Lalilo
Link to $https://twitter.com/LaliloAppLink to $https://github.com/lalaliloLink to $https://www.instagram.com/lalilo_lecture/Link to $https://www.linkedin.com/company/lalilo/