loading...

Typescript and Redux. My tips.

pretaporter profile image Maksim Updated on ・3 min read

Introduction

Hello everybody!

Today I want to talk about quite popular technologies. Typescript and Redux. Both helps to develop fault tolerant applications. There is a lot of approaches to write typings of state and actions. I formed own, that could save your time.

State

Each state in Redux should be immutable. Immutable object cannot be modified after it is created. If you forget this rule, your component do not rerender after state changes. So let's use Readonly modifier. It makes all properties readonly. You can't mutate property in reducer.

export type State = Readonly<{
  value: number;
}>

Do not forget use Readonly modifier for nested objects too. But what about arrays. For example:

export type State = Readonly<{
  list: number[];
}>

You still can change it. Let's fix it, TypeScript includes special modifier ReadonlyArray.

export type State = Readonly<{
  list: ReadonlyArray<number>;
}>

Now you can't add or remove items. You have to create new array for changes. Also TypeScript has special modifiers for Map and Set: ReadonlyMap and ReadonlySet.

Actions

I use enums for Redux actions. Naming convention is simple: @namespace/effect. Effect always in past tense, because it is something already happened. For example, @users/RequestSent, @users/ResponseReceived, @users/RequestFailed...

enum Action {
  ValueChanged = '@counter/ValueChanged',
}

Action creators

Little magic starts.

The first thing, we use const assertions. The const assertion allowed TypeScript to take the most specific type of the expression.

The second thing, we extract return types of action creators by type inference.

const actions = {
  setValue(value: number) {
    return {
      payload: value,
      type: Action.ValueChanged,
    } as const;
  },
}

type InferValueTypes<T> = T extends { [key: string]: infer U } ? U : never;

type Actions = ReturnType<InferValueTypes<typeof actions>>;

Let's improve it by helper function:

export function createAction<T extends string>(
  type: T,
): () => Readonly<{ type: T }>;
export function createAction<T extends string, P>(
  type: T,
): (payload: P) => Readonly<{ payload: P; type: T }>;
export function createAction<T extends string, P>(type: T) {
  return (payload?: P) =>
    typeof payload === 'undefined' ? { type } : { payload, type };
}

Then our actions object will look:

const actions = {
  setValue: createAction<Action.ValueChanged, number>(Action.ValueChanged)
}

Reducers

Inside reducer we just use things described before.

const DEFAULT_STATE: State = 0;

function reducer(state = DEFAULT_STATE, action: Actions): State {
  if (action.type === Action.ValueChanged) {
    return action.payload;
  }

  return state;
}

Now, for all of your critical changes inside action creators, TypeScript throws error inside reducer. You will have to change your code for correct handlers.

Module

Each module exports object like this:

export const Module = {
  actions,
  defaultState: DEFAULT_STATE,
  reducer,
}

You can also describe your saga inside module, if you use redux-saga.

Configure store

Describe the whole state of application, all actions and store.

import { Store } from 'redux';

type AppState = ModuleOneState | ModuleTwoState;
type AppActions = ModuleOneActions | ModuleTwoActions;

type AppStore = Store<AppState, AppActions>;

Hooks

If you use hooks from react-redux, it would be helpful too.
By default you need to describe typings each time, when you use this hooks. Better to make it one time.

export function useAppDispatch() {
  return useDispatch<Dispatch<AppActions>>();
}

export function useAppSelector<Selected>(
  selector: (state: AppState) => Selected,
  equalityFn?: (left: Selected, right: Selected) => boolean,
) {
  return useSelector<AppState, Selected>(selector, equalityFn);
}

Now you can't dispatch invalid action.

The end

I hope all of this things will make your live easier.
I will glad for your comments and questions.
My twitter.

Posted on by:

pretaporter profile

Maksim

@pretaporter

Creator of https://github.com/ibitcy/eo-locale. Frontend developer. Son, husband and cat owner.

Discussion

markdown guide
 

type InferValueTypes = T extends { [key: string]: infer U } ? U : never;

regarding this, i have a hard time understanding where the U comes from, and why the ternary is used?

 

We need to collect all our return types. Straight approach is union:

ReturnType<typeof actionCreatorOne> | ReturnType<typeof actionCreatorTwo>

Let's automate this. We use extends for checking is object.

const someObj = { field: 'Hello World!' };

type isObject<T> = T extends { [key: string]: unknown } ? T : never;

isObject<typeof value>; // { field: 'Hello World!' }
isObject<'Hello World!'>; // never

And then we should extract object values:

type InferValueTypes<T> = T extends { [key: string]: infer U } ? U : never;

Almost the same as isObject

 

ahh i c so the β€˜T extends { [key: string] infer U }’ will evaluate to true if T does extends the provided object shape.
thnks