React с TypeScript означает, что компоненты пишутся в файлах .tsx, где у пропсов, состояния, рефов и обработчиков событий есть типы, поэтому компилятор ловит пропущенный пропс или неправильный обработчик до запуска кода. Типы исчезают при сборке: в браузере выполняется тот же JavaScript, который вы написали бы без них. Живые редакторы на этой странице запускают этот JavaScript, а типизированная версия стоит рядом с каждым в статическом блоке.
Вот тот же Badge с типами. count необязателен (?), поэтому второй значок берёт значение по умолчанию 0, а children принимает и элемент, и обычную строку.
type BadgeProps = {
label: string;
count?: number;
children: React.ReactNode;
};
function Badge({ label, count = 0, children }: BadgeProps) {
return (
<div>
<strong>{label}</strong> ({count})
<div>{children}</div>
</div>
);
}
<Badge count={3}>Hi</Badge>;
// Error: Property 'label' is missing in type '{ children: string; count: number; }'
Создание проекта на TypeScript
У Vite есть шаблон React плюс TypeScript:
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
Вы получаете файлы .tsx, tsconfig.json с включённым strict и уже установленные определения типов React (@types/react, @types/react-dom). Vite удаляет типы при раздаче и сборке, но не проверяет их; скрипт build шаблона сначала запускает tsc, а редактор проверяет по мере ввода. Фреймворки вроде Next.js настраивают TypeScript так же, когда вы создаёте проект через их CLI.
Типизация пропсов
Опишите пропсы как тип объекта и аннотируйте деструктурированный параметр. Несколько шаблонов покрывают большинство компонентов:
interface UserCardProps {
name: string; // required
age?: number; // optional, may be undefined
role: 'admin' | 'member'; // a union of allowed strings
tags: string[];
onSelect: (id: string) => void; // a callback prop
children?: React.ReactNode; // anything React can render
}
function UserCard({ name, age, role, tags, onSelect, children }: UserCardProps) {
// ...
}
type или interface. Для пропсов подходят оба. interface можно расширять через extends и объединять, объявив дважды; type также может выражать объединения, пересечения и сопоставленные типы. Большинство команд выбирают одно и используют его везде.
children. Используйте React.ReactNode для всего, что идёт между тегами: элементов, строк, чисел, массивов, null. Как вообще приходит children, объясняет страница о пропсах.
Почему React.FC необязателен. const Badge: React.FC<BadgeProps> = (...) => ... типизирует всю функцию, а не параметр. Начиная с типов React 18 он больше не добавляет children за вас, не может принимать параметр типа для обобщённого компонента и не добавляет ничего, чего не даёт типизированный параметр. В старом коде он используется широко; для нового кода обычный выбор это простые функции с типизированными пропсами.
Типизация useState
Для простого начального значения достаточно вывода типов: useState(0) это number, useState('') это string. Передавайте аргумент типа, когда состояние может хранить больше, чем подсказывает начальное значение: значение, которое начинается с null, или одну из нескольких фиксированных строк.
Нажмите «Load user», и статус перейдёт от idle к loading и к done. Типизированное состояние:
type User = { id: number; name: string };
type Status = 'idle' | 'loading' | 'done';
const [user, setUser] = useState<User | null>(null);
const [status, setStatus] = useState<Status>('idle');
user.name; // Error: 'user' is possibly 'null'
user?.name; // OK
setStatus('ready'); // Error: Argument of type '"ready"' is not assignable to parameter of type 'SetStateAction<Status>'
Без <User | null> useState(null) выводит тип null, и setUser(data) не компилируется. Объединение заставляет обработать пустой случай, и это ровно проверка user ? ... : ... в работающем примере.
useRef для элементов DOM
Ref, который указывает на элемент DOM, принимает тип элемента и начинается с null, потому что элемента не существует, пока React не присоединит его после первого рендера.
Напишите что-нибудь, затем нажмите «Log the value»: консоль выведет то, что вы ввели, прочитанное прямо из элемента DOM. Типизированная версия:
const inputRef = useRef<HTMLInputElement>(null);
// inputRef.current is HTMLInputElement | null
inputRef.current?.focus(); // OK
inputRef.current.focus(); // Error: 'inputRef.current' is possibly 'null'
const timerId = useRef<number | null>(null); // a mutable value, not a DOM node
Используйте интерфейс элемента, соответствующий тегу: HTMLInputElement, HTMLDivElement, HTMLCanvasElement, HTMLButtonElement. В типах React 19 useRef всегда принимает аргумент и возвращает RefObject, у которого можно присваивать current, поэтому одна и та же форма работает и для рефов на DOM, и для значений вроде id таймера. Сторону выполнения разбирает страница о useRef.
Типы обработчиков событий
Обработчикам прямо в строке аннотация не нужна: в onChange={(e) => setName(e.target.value)} TypeScript знает e из пропса, в который он передан. Аннотируйте, когда выносите обработчик в отдельную функцию.
Отправьте форму, и консоль выведет адрес; preventDefault не даёт странице перезагрузиться. С типами:
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setEmail(e.target.value); // e.target is HTMLInputElement, value is string
}
function handleSubmit(e: React.SubmitEvent<HTMLFormElement>) {
e.preventDefault();
}
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {}
function handleKey(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') { /* ... */ }
}
Начиная с @types/react 19.2.10 onSubmit типизирован через React.SubmitEvent, а более старый React.FormEvent помечен как устаревший; со старой версией типов пишите вместо этого React.FormEvent<HTMLFormElement>. Параметр типа это элемент, к которому прикреплён обработчик. Если вы не уверены в типе, наведите курсор на пропс onChange в редакторе или типизируйте весь обработчик сразу: const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { ... }.
Типизация контекста
Передайте createContext тип значения. Если есть разумное значение по умолчанию, передайте его, и каждый потребитель получит значение, отличное от null:
type Theme = 'light' | 'dark';
const ThemeContext = createContext<Theme>('light');
const theme = useContext(ThemeContext); // Theme
Когда хорошего значения по умолчанию нет (вошедший пользователь, хранилище), начните с null и оберните useContext в хук, который выбрасывает ошибку, если компонент используется вне провайдера. Тогда каждый вызывающий получает тип без null и понятное сообщение об ошибке вместо падения где-то в глубине.
type Auth = { user: string; logout: () => void };
const AuthContext = createContext<Auth | null>(null);
export function useAuth(): Auth {
const value = useContext(AuthContext);
if (!value) throw new Error('useAuth must be used inside <AuthProvider>');
return value;
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState('Ada');
return (
<AuthContext value={{ user, logout: () => setUser('') }}>
{children}
</AuthContext>
);
}
<AuthContext value={...}> это синтаксис провайдера в React 19; <AuthContext.Provider value={...}> типизируется так же.
Обобщённые компоненты
Список, таблица или select, которые работают с любым типом элементов, это обобщённый (generic) компонент: параметр типа связывает пропс items с колбэком renderItem.
Типизированный List:
type ListProps<T> = {
items: T[];
renderItem: (item: T) => React.ReactNode;
getKey: (item: T) => string | number;
};
function List<T>({ items, renderItem, getKey }: ListProps<T>) {
return <ul>{items.map((item) => <li key={getKey(item)}>{renderItem(item)}</li>)}</ul>;
}
<List items={books} getKey={(b) => b.isbn} renderItem={(b) => b.title} />;
// b is inferred as the book type, so b.titel would be an error
TypeScript выводит T из items, поэтому вызывающим никогда не нужно писать параметр типа. В файле .tsx стрелочной функции нужен <T,> вместо <T>, потому что <T> сам по себе читается как тег JSX.
Расширение пропсов нативных элементов
Обёртка вокруг button или input должна принимать каждый атрибут, который принимает нативный элемент. React.ComponentProps<'button'> это полный набор; добавьте поверх свои пропсы и разверните остальные через spread.
type ButtonProps = React.ComponentProps<'button'> & {
variant?: 'primary' | 'ghost';
};
function Button({ variant = 'primary', style, ...rest }: ButtonProps) {
return (
<button
{...rest}
style={{ fontWeight: variant === 'primary' ? 600 : 400, ...style }}
/>
);
}
<Button type="submit" disabled onClick={(e) => console.log(e.currentTarget)}>
Save
</Button>;
onClick, disabled, type и aria-* типизированы, хотя вы их не перечисляли. В React 19 ref это обычный пропс, и ComponentProps<'button'> его включает, поэтому <Button ref={buttonRef}> доходит до узла DOM через spread без forwardRef. Используйте ComponentPropsWithoutRef<'button'>, когда хотите исключить ref, и ComponentProps<typeof UserCard>, чтобы прочитать пропсы одного из ваших собственных компонентов.
Частые ошибки типов
"'ref.current' is possibly 'null'". Во время первого рендера элемента не существует. Используйте ref.current?.focus() или проверяйте if (ref.current) перед использованием.
"Type 'string' is not assignable" у состояния-объединения. const [status, setStatus] = useState('idle') выводит string, что шире, чем нужно, и передача его в пропс с типом Status не проходит. Пишите useState<Status>('idle').
Событие с типом не того элемента. React.ChangeEvent<HTMLInputElement> на select даёт e.target неправильный тип. Параметр типа должен совпадать с элементом, к которому прикреплён обработчик.
any, чтобы заглушить ошибку. Он отключает проверку для всего, к чему прикасается это значение. Предпочитайте unknown и сужайте его или исправляйте тип в источнике.
Часто задаваемые вопросы
Как создать приложение React с TypeScript?
Выполните npm create vite@latest my-app -- --template react-ts, затем npm install и npm run dev. Компоненты пишутся в файлах .tsx, а Vite при сборке удаляет типы; чтобы проверить их, запустите tsc (это делает скрипт build шаблона).
Какой тип у children в React?
React.ReactNode. Он принимает всё, что React умеет рендерить: элементы, строки, числа, массивы, null и undefined. Используйте React.ReactElement, только когда нужен ровно один элемент.
Как типизировать useState с null?
Передайте тип явно как объединение: useState<User | null>(null). Без аргумента типа TypeScript выводит null как единственное допустимое значение.
Какой тип у события onChange в React?
React.ChangeEvent<HTMLInputElement> для input (HTMLTextAreaElement или HTMLSelectElement для этих элементов). Тогда e.target.value имеет тип string.
Стоит ли использовать React.FC?
Это необязательно. Типизация параметра пропсов напрямую (function Card({ title }: CardProps)) делает то же самое, читается проще и работает с обобщениями. React.FC больше не добавляет children неявно, поэтому даёт мало дополнительного.
Использовать type или interface для пропсов?
Подходит и то, и другое. interface можно расширять и объединять; type может выражать объединения и сопоставленные типы. Выберите одно соглашение для проекта и придерживайтесь его.