TypeScript виводить тип стану з початкового значення, і найчастіше явний тип не потрібен:
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string
const [open, setOpen] = useState(false); // boolean
Коли виведення не спрацьовує:
1. Початкове null - значення з'явиться пізніше:
type User = { id: number; name: string };
const [user, setUser] = useState<User | null>(null);
if (user) {
user.name; // після перевірки - User
}
Без явного типу useState(null) дасть тип null, і setUser(data) не скомпілюється.
2. Порожній масив:
const [items, setItems] = useState<Product[]>([]);
Без параметра useState([]) дає never[] - у такий масив нічого не додати.
3. Обмежений набір значень (union):
type Status = 'idle' | 'loading' | 'success' | 'error';
const [status, setStatus] = useState<Status>('idle');
Без явного типу стан стане string, і setStatus('sucess') з помилкою пройде.
Стани, що залежать один від одного, краще поєднати в один з дискримінованим union - тоді неможливі комбінації не скомпілюються:
type RequestState =
| { status: 'loading' }
| { status: 'success'; data: User[] }
| { status: 'error'; error: string };
const [state, setState] = useState<RequestState>({ status: 'loading' });
if (state.status === 'success') {
state.data; // доступно лише тут
}
Окремі isLoading, error, data дозволяють стан «завантажується, але з помилкою й даними», а union - ні.
Функція оновлення типізується автоматично: setCount((c) => c + 1) знає, що c - число.
Лінива ініціалізація теж виводить тип з результату: useState(() => readFromStorage()).
Пастка: useState<User>({} as User) - «обіцянка» компілятору, що порожній об'єкт - повноцінний User. Помилки звернення до відсутніх полів з'являться під час виконання, а не компіляції. Чесніше User | null.