Типизация кастомного хука, возвращающего кортеж в стиле useState
useToggle обязан отдавать вызывающему ту же форму, что и useState, — кортеж из двух слотов, где первый элемент boolean, а второй — функция переключения без аргументов.
Ограничение: const [on, toggle] = useToggle() должен дать on: boolean и toggle: () => void, а не boolean | (() => void) в обоих слотах. Никаких as на месте вызова, никакого any.
import { useCallback, useState } from 'react';
export function useToggle(initial = false) {
const [on, setOn] = useState(initial);
const toggle = useCallback(() => setOn((v) => !v), []);
// ваш код здесь
}
Допишите реализацию.
Зафиксируйте кортеж через as const — return [on, toggle] as const — или аннотируйте тип возврата как [boolean, () => void]. Без одного из двух TypeScript расширит литерал до (boolean | (() => void))[], и деструктуризация отдаст это объединение в оба слота.
- ✗Ждать, что
return [on, toggle]выведется кортежем, а не расширится до массива - ✗Хвататься за приведение на месте вызова вместо починки типа возврата самого хука
- ✗Считать, что
as constделает возвращённую функцию непригодной как колбэк
- →Почему сам
useStateвозвращает кортеж, а не массив объединения? - →Когда из кастомного хука лучше возвращать именованный объект, а не кортеж?
Решение
import { useCallback, useState } from 'react';
export function useToggle(initial = false) {
const [on, setOn] = useState(initial);
const toggle = useCallback(() => setOn((v) => !v), []);
return [on, toggle] as const;
// ^? readonly [boolean, () => void]
}
const [on, toggle] = useToggle();
// ^boolean ^() => void
Почему без as const ломается
return [on, toggle] — это литерал изменяемого массива. TypeScript расширяет его до (boolean | (() => void))[]: длина забыта, позиции забыты. Деструктуризация после этого даёт объединение в обоих слотах, и toggle() перестаёт вызываться, а if (on) перестаёт сужаться.
as const даёт readonly [boolean, () => void] — длина и позиции сохранены. Эквивалент без as const — явная аннотация возврата:
export function useToggle(initial = false): [boolean, () => void] {
const [on, setOn] = useState(initial);
const toggle = useCallback(() => setOn((v) => !v), []);
return [on, toggle];
}
Разница только в readonly: вариант с as const запрещает вызывающему присваивать в слоты кортежа, аннотация — разрешает. На саму функцию toggle as const не влияет: он фиксирует тип массива, а не замораживает объекты в нём.