Урок 08

Типизация useReducer

Базовая типизация

type State = {
    count: number;
};

type Action =
    | { type: 'increment' }
    | { type: 'decrement' }
    | { type: 'set'; payload: number };

function reducer(state: State, action: Action): State {
    switch (action.type) {
        case 'increment':
            return { count: state.count + 1 };
        case 'decrement':
            return { count: state.count - 1 };
        case 'set':
            return { count: action.payload };
    }
}

const [state, dispatch] = useReducer(reducer, { count: 0 });

TypeScript выводит State и Action из сигнатуры редьюсера.

Явное указание

const [state, dispatch] = useReducer<typeof reducer>(reducer, { count: 0 });

Обычно не нужно.

Dispatch

import type { Dispatch } from 'react';

type DispatchType = Dispatch<Action>;

Dispatch<Action> принимает только допустимые действия:

dispatch({ type: 'increment' });      // ок
dispatch({ type: 'set', payload: 5 }); // ок
dispatch({ type: 'invalid' });         // ошибка
dispatch({ type: 'set' });             // ошибка: нет payload

Ленивая инициализация

type State = { count: number };

function init(initialCount: number): State {
    return { count: initialCount };
}

const [state, dispatch] = useReducer(reducer, 0, init);

init получает второй аргумент useReducer и возвращает начальное состояние.

Generic-reducer

function createReducer<S, A extends { type: string }>() {
    return (state: S, action: A): S => {
        return state;
    };
}

Action creators

const actions = {
    increment: (): Action => ({ type: 'increment' }),
    decrement: (): Action => ({ type: 'decrement' }),
    set: (payload: number): Action => ({ type: 'set', payload }),
};

dispatch(actions.set(5));

Типизация через discriminated union

Самый распространённый способ — каждый action — отдельный тип с дискриминатором:

type Action =
    | { type: 'add'; item: Item }
    | { type: 'remove'; id: number }
    | { type: 'update'; id: number; patch: Partial<Item> }
    | { type: 'clear' };

function reducer(state: Item[], action: Action): Item[] {
    switch (action.type) {
        case 'add':
            return [...state, action.item];
        case 'remove':
            return state.filter((i) => i.id !== action.id);
        case 'update':
            return state.map((i) => (i.id === action.id ? { ...i, ...action.patch } : i));
        case 'clear':
            return [];
    }
}

В каждом case доступны только релевантные поля.

Exhaustive check

function reducer(state: State, action: Action): State {
    switch (action.type) {
        case 'increment': return { count: state.count + 1 };
        case 'decrement': return { count: state.count - 1 };
        case 'set': return { count: action.payload };
        default:
            const _exhaustive: never = action;
            return state;
    }
}

Если добавишь новый тип action и забудешь обработать — TypeScript ошибётся.

Контекст с reducer

type State = { count: number };
type Action = { type: 'increment' } | { type: 'decrement' };

const StateContext = createContext<State | null>(null);
const DispatchContext = createContext<Dispatch<Action> | null>(null);

Итоги

  • Типы выводятся из редьюсера
  • Dispatch<Action> — тип диспатча
  • Discriminated union — основной паттерн
  • never для exhaustive check
  • Ленивая инициализация — третий аргумент
  • Action creators дают удобство