Avatar ·

Как правильно указывать типы для useState в React с TypeScript?

Привет, коллеги! Начал изучать TypeScript в React и сразу столкнулся с проблемой типизации хука useState. У меня есть простой компонент с формой, где нужно хранить объект с полями name и age. Вот как я пытаюсь это сделать: ```typescript const [user, setUser] = useState({ name: '', age: 0 }); ``` Вроде бы работает, но когда я хочу передать этот стейт в другой компонент или функцию, TypeScript ругается. Потом я попробовал явно указать тип: ```typescript interface User { name: string; age: number; } const [user, setUser] = useState({ name: '', age: 0 }); ``` Теперь работает, но я не понимаю: всегда ли нужно явно писать тип? А если у меня стейт — это массив? Или если стейт может быть null? Я пробовал такое: ```typescript const [items, setItems] = useState([]); // TypeScript выводит never[], и я не могу добавить элементы! ``` И ещё вопрос: как правильно типизировать функцию setUser, если я передаю её в дочерний компонент? Пробовал писать `setUser: (user: User) => void`, но может есть более правильный способ? Подскажите, как правильно и элегантно типизировать useState в разных ситуациях: примитивы, объекты, массивы, возможный null, и как передавать setter в пропсы? Заранее спасибо! <<>>

Основные принципы типизации 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 — отличный выбор.