Как правильно указывать типы для useState в React с TypeScript?
Основные принципы типизации useState
TypeScript выводит тип автоматически из начального значения. Однако есть важные нюансы, которые нужно знать.
1. Примитивные типы (string, number, boolean)
Для примитивов TypeScript отлично выводит тип автоматически. Явная типизация обычно не нужна:
const [name, setName] = useState(''); // тип: string
const [count, setCount] = useState(0); // тип: number
const [isActive, setIsActive] = useState(true); // тип: boolean
Но если начальное значение может быть разных типов, лучше указать явно:
const [value, setValue] = useState<string | number>('');
2. Объекты и интерфейсы
Для сложных объектов всегда создавайте интерфейс или type:
interface User {
id: number;
name: string;
age: number;
email?: string; // необязательное поле
}
const [user, setUser] = useState<User>({
id: 1,
name: 'Иван',
age: 25
});
Если объект может быть null, используйте union-тип:
const [user, setUser] = useState<User | null>(null);
3. Массивы
Самая частая ошибка — пустой массив без типа. TypeScript выводит never[], и вы не сможете добавить элементы:
// ❌ Неправильно
const [items, setItems] = useState([]);
// items: never[] — нельзя добавить элементы
// ✅ Правильно
const [items, setItems] = useState<string[]>([]);
// или с интерфейсом
interface Todo {
id: number;
text: string;
completed: boolean;
}
const [todos, setTodos] = useState<Todo[]>([]);
Если массив изначально не пустой, тип выведется автоматически:
const [items, setItems] = useState(['a', 'b']); // тип: string[]
4. Типизация setter-функции при передаче в пропсы
Используйте тип Dispatch<SetStateAction<T>> из React:
import { Dispatch, SetStateAction } from 'react';
interface Props {
user: User;
setUser: Dispatch<SetStateAction<User>>;
}
// Или более краткий вариант:
interface Props {
user: User;
setUser: React.Dispatch<React.SetStateAction<User>>;
}
Этот тип покрывает все варианты: передача нового значения или функции-обновления:
setUser({ ...user, name: 'Петр' }); // ✅ новое значение
setUser(prev => ({ ...prev, age: prev.age + 1 })); // ✅ функция
5. Специальные случаи
Стейт с функцией как начальное значение:
const [value, setValue] = useState<number>(() => {
const initial = computeExpensiveValue();
return initial;
});
Generic-компоненты с useState:
function List<T>() {
const [items, setItems] = useState<T[]>([]);
// ...
}
Рекомендации
- Всегда явно типизируйте пустые массивы и возможные null-значения
- Для объектов создавайте отдельные интерфейсы
- Используйте
Dispatch<SetStateAction<T>>для setter-функций в пропсах - Не злоупотребляйте
any— это убивает смысл TypeScript
Альтернативный подход: использование useReducer для сложной логики
Если ваш стейт становится слишком сложным (много полей, частые обновления), рассмотрите useReducer вместо useState. Это даёт более строгую типизацию и предсказуемость:
interface UserState {
name: string;
age: number;
email: string;
}
type UserAction =
| { type: 'SET_NAME'; payload: string }
| { type: 'SET_AGE'; payload: number }
| { type: 'SET_EMAIL'; payload: string }
| { type: 'RESET' };
function userReducer(state: UserState, action: UserAction): UserState {
switch (action.type) {
case 'SET_NAME':
return { ...state, name: action.payload };
case 'SET_AGE':
return { ...state, age: action.payload };
case 'SET_EMAIL':
return { ...state, email: action.payload };
case 'RESET':
return { name: '', age: 0, email: '' };
default:
return state;
}
}
// Использование:
const [user, dispatch] = useReducer(userReducer, {
name: '',
age: 0,
email: ''
});
dispatch({ type: 'SET_NAME', payload: 'Анна' });
Преимущества этого подхода:
- Все возможные действия строго типизированы
- Логика обновления централизована
- Легче тестировать и отлаживать
- Лучше подходит для сложных форм или состояний с множеством взаимосвязанных полей
Для простых случаев (1-2 поля) вполне достаточно useState, но для сложных форм с валидацией и зависимостями useReducer — отличный выбор.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги