DEV Community

Maksim
Maksim

Posted on • Edited on

Typescript and Redux. My tips.

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;
}>
Enter fullscreen mode Exit fullscreen mode

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

export type State = Readonly<{
  list: number[];
}>
Enter fullscreen mode Exit fullscreen mode

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

export type State = Readonly<{
  list: ReadonlyArray<number>;
}>
Enter fullscreen mode Exit fullscreen mode

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',
}
Enter fullscreen mode Exit fullscreen mode

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>>;
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode

Then our actions object will look:

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

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;
}
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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>;
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (3)

Collapse
 
andrasnyarai profile image
Andras Nyarai

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?

Collapse
 
pretaporter profile image
Maksim

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

Collapse
 
andrasnyarai profile image
Andras Nyarai

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