Типизация состояния state
Сегодня мы создадим интерфейсы для состояния, разберемся, как типизировать действия и научимся практическому применению типизации в реальном приложении.
Зачем вообще типизировать состояние?
Представьте себе гостиницу, где нет списка гостей. Администратор не знает, кто заселился и кто уехал. Примерно так же работает приложение без типизации: мы не знаем, какие данные наш редьюсер вообще поддерживает.
Типизация состояния state помогает:
- определить структуру данных, которые редьюсер будет обрабатывать;
- предотвратить ошибки, когда случайно пытаетесь обратиться к несуществующему полю;
- облегчить процесс написания функций, работающих с состоянием.
Пример типизации простого состояния
Начнем с небольшого примера. Допустим, мы разрабатываем Todo-приложение, где есть список задач и флаг загрузки данных. Вот как можно описать состояние с помощью TypeScript:
// Интерфейс описывает состояние
interface TodoState {
todos: { id: number; text: string; completed: boolean }[];
isLoading: boolean;
}
// Пример начального состояния
const initialState: TodoState = {
todos: [],
isLoading: false,
};
Здесь TodoState — это интерфейс, который описывает данные нашего состояния:
todos— это массив объектов, где каждый объект представляет задачу.isLoading— булево значение, отвечающее за отображение состояния загрузки.
Применяя такой интерфейс, вы всегда уверены, что состояние точно соответствует ожидаемой структуре.
Типизация действий action
Что такое "action" в контексте useReducer?
Action — это то, что инициирует любые изменения в состоянии. Это объект, где мы указываем:
- тип действия (обычно строка, например:
"ADD_TODO"или"TOGGLE_TODO"); - дополнительные данные (например, текст новой задачи или идентификатор задачи).
Каждое действие сообщает редьюсеру: "Привет, что-то изменилось! Вот подробности."
Как типизировать действия?
Для начала определим типы допустимых действий. Например, в нашем Todo-приложении:
- Добавление новой задачи.
- Переключение состояния задачи (выполнено или не выполнено).
- Загрузка задач.
Мы можем создать тип для действий несколькими способами. Один из самых удобных подходов — это использование TypeScript union types (объединений).
// Определяем типы действий
type TodoAction =
| { type: 'ADD_TODO'; payload: { text: string } } // Для добавления новой задачи
| { type: 'TOGGLE_TODO'; payload: { id: number } } // Для переключения выполнения задачи
| { type: 'SET_LOADING'; payload: { isLoading: boolean } }; // Установка флага загрузки
Давайте разберем пример:
type— это обязательное поле, которое указывает на тип действия.payload— это объект с дополнительной информацией, необходимой для выполнения действия, и он тоже типизирован.
Пример использования типизированных действий в редьюсере
Когда у нас есть типизированные действия, их легко использовать в редьюсере. Например:
// Редьюсер с типизировным состоянием и действиями
const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
switch (action.type) {
case 'ADD_TODO':
// Добавляем новую задачу в массив
return {
...state,
todos: [
...state.todos,
{ id: Date.now(), text: action.payload.text, completed: false },
],
};
case 'TOGGLE_TODO':
// Ищем задачу по id и переключаем её completed
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
),
};
case 'SET_LOADING':
// Изменяем флаг загрузки
return {
...state,
isLoading: action.payload.isLoading,
};
default:
return state;
}
};
Обратите внимание, как TypeScript помогает:
- Подсвечивает ошибки, если вы используете неправильный тип действия.
- Уверяет, что
payloadпередается с правильной структурой.
А что если действие не передает payload?
Иногда действиям не требуется никаких дополнительных данных. Например, сброс состояния к начальному. В таких случаях достаточно указать только тип действия:
type ClearAction = { type: 'CLEAR_STATE' };
Пример: объединяем типизацию состояния и действий
Теперь давайте создадим небольшой пример с использованием useReducer. Мы создадим компонент TodoList, где можно добавлять и переключать задачи.
import React, { useReducer } from 'react';
// Типизация состояния
interface TodoState {
todos: { id: number; text: string; completed: boolean }[];
isLoading: boolean;
}
// Типизация действий
type TodoAction =
| { type: 'ADD_TODO'; payload: { text: string } }
| { type: 'TOGGLE_TODO'; payload: { id: number } };
// Начальное состояние
const initialState: TodoState = {
todos: [],
isLoading: false,
};
// Редьюсер
const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{ id: Date.now(), text: action.payload.text, completed: false },
],
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
),
};
default:
return state;
}
};
// Компонент с TodoList
const TodoList: React.FC = () => {
const [state, dispatch] = useReducer(todoReducer, initialState);
const addTodo = () => {
const text = prompt('Введите текст задачи:');
if (text) {
dispatch({ type: 'ADD_TODO', payload: { text } });
}
};
const toggleTodo = (id: number) => {
dispatch({ type: 'TOGGLE_TODO', payload: { id } });
};
return (
<div>
<h2>Список задач</h2>
<button onClick={addTodo}>Добавить задачу</button>
<ul>
{state.todos.map(todo => (
<li
key={todo.id}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
onClick={() => toggleTodo(todo.id)}
>
{todo.text}
</li>
))}
</ul>
</div>
);
};
export default TodoList;
Типичные ошибки и пути их решения
Одна из часто встречающихся ошибок — отсутствие типизации для action.type и action.payload. Например, если случайно передать неверный тип действия, TypeScript не предупредит вас, если типы не были описаны. Поэтому всегда создавайте строгую типизацию для всех возможных действий.
Еще одна проблема — забыть типизировать state. Если вы не зададите интерфейс состояния, то, скорее всего, столкнетесь с ошибками, обращаясь к полям, которых не существует.
Наконец, избегайте огромных редьюсеров с сотнями условий case. Это не только усложняет код, но и делает его практически непроверяемым.
Ну что, теперь вы вооружены более глубокими знаниями о типизации в useReducer!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ