TypeScript с React
В этой главе показано, как начать проект на React с TypeScript и как типизировать то, с чем вы сталкиваетесь каждый день: пропсы, события, состояние, ссылки (refs), дочерние элементы, загрузку данных и контекст.
Зачем использовать TypeScript с React?
TypeScript улучшает React благодаря:
- Типобезопасности пропсов, состояния и контекста
- Лучшему автодополнению и рефакторингу в IDE
- Раннему обнаружению ошибок — ещё во время написания кода
Примечание: В этой главе предполагается, что вы знаете основы React. Если вы новичок в React, сначала пройдите учебник по React.
Начало работы
Создайте новое приложение React + TypeScript с помощью Vite, выполнив эти команды:
npm create vite@latest my-app -- --template react-tscd my-appnpm installnpm run dev
В вашем tsconfig.json должны быть такие рекомендуемые compilerOptions:
"target": "ES2020""lib": ["ES2020", "DOM", "DOM.Iterable"]"module": "ESNext""moduleResolution": "Node""jsx": "react-jsx""strict": true"skipLibCheck": true"noEmit": true"resolveJsonModule": true"allowSyntheticDefaultImports": true"esModuleInterop": true"forceConsistentCasingInFileNames": true
и рядом с ними "include": ["src"].
Примечание: Оставьте strict включённым для наилучшей типобезопасности. Эти параметры хорошо работают с Vite и Create React App.
Типизация компонентов
Опишите пропсы с помощью типа, а затем используйте этот тип в функциональном компоненте:
Типизированные пропсы в функциональном компоненте
В GreetingProps есть обязательное поле name и необязательное age; компонент выводит строку с возрастом только тогда, когда он передан.
// Greeting.tsx
type GreetingProps = {
name: string;
age?: number;
};
export function Greeting({ name, age }: GreetingProps) {
return (
<div>
<h2>Hello, {name}!</h2>
{age !== undefined && <p>You are {age} years old</p>}
</div>
);
}
Распространённые приёмы
Это приёмы типизации, которые вы чаще всего будете использовать в приложении на React.
Типобезопасные события. Задавайте обработчикам событий для полей ввода и кнопок подходящий тип события React.
Типизация состояния с useState. Используйте явные аргументы типа для чисел, объединений и значений, допускающих null.
useRef с элементами DOM. Типизируйте ref тем элементом DOM, который он будет хранить, чтобы безопасно читать его свойства.
Типизация дочерних элементов. Принимайте children с типом React.ReactNode.
Вспомогательные функции загрузки с дженериками. Используйте дженерик-функцию, чтобы типизировать ответы API.
Минимальный контекст и свой хук. Предоставьте небольшой типизированный контекст и вспомогательный хук, который гарантирует его доступность.
Типы TypeScript для Vite: Добавьте глобальные типы Vite, чтобы ничего не отсутствовало. Создайте файл src/vite-env.d.ts с содержимым /// <reference types="vite/client" /> или добавьте "types": ["vite/client"] в compilerOptions в tsconfig.json.
О React.FC: Предпочитайте обычные функциональные компоненты с явной типизацией. React.FC необязателен; он неявно добавляет children, но использовать его не требуется.
Необязательные baseUrl и paths: Они позволяют сделать импорты короче, если ваш сборщик это поддерживает. Задайте "baseUrl": "." и "paths": { "@/*": ["src/*"] } в compilerOptions. Настраивайте их только если ваши инструменты (например, Vite или tsconfig-paths) настроены на псевдонимы путей.
Типобезопасные обработчики событий
React.ChangeEvent<HTMLInputElement> и React.MouseEvent<HTMLButtonElement> дают типизированный доступ к событию и его цели.
// Input change
function NameInput() {
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value);
}
return <input onChange={handleChange} />;
}
// Button click
function SaveButton() {
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
e.preventDefault();
}
return <button onClick={handleClick}>Save</button>;
}
Типизация состояния с useState
Число, объединение строковых литералов и объект, допускающий null, — у каждого явный аргумент типа.
const [count, setCount] = React.useState<number>(0);
const [status, setStatus] = React.useState<'idle' | 'loading' | 'error'>('idle');
type User = { id: string; name: string };
const [user, setUser] = React.useState<User | null>(null);
useRef с элементом DOM
Ссылка типизирована как HTMLInputElement, поэтому вызов inputRef.current?.select() проверяется компилятором.
function FocusInput() {
const inputRef = React.useRef<HTMLInputElement>(null);
return <input ref={inputRef} onFocus={() => inputRef.current?.select()} />;
}
Типизация дочерних элементов
children — необязательное поле с типом React.ReactNode, поэтому принимается всё, что React умеет отображать.
type CardProps = { title: string; children?: React.ReactNode };
function Card({ title, children }: CardProps) {
return (
<div>
<h2>{title}</h2>
{children}
</div>
);
}
Дженерик-функция для загрузки данных
fetchJson<T> возвращает Promise<T>, поэтому форму ответа определяет вызывающий код.
async function fetchJson<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error('Network error');
return res.json() as Promise<T>;
}
// Usage inside an async function/component effect
async function loadPosts() {
type Post = { id: number; title: string };
const posts = await fetchJson<Post[]>("/api/posts");
console.log(posts);
}
Типизированный контекст и свой хук
Значение контекста — это либо объект темы, либо null; useTheme выбрасывает ошибку, если используется вне провайдера.
type Theme = 'light' | 'dark';
const ThemeContext = React.createContext<{ theme: Theme; toggle(): void } | null>(null);
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = React.useState<Theme>('light');
const value = { theme, toggle: () => setTheme(t => (t === 'light' ? 'dark' : 'light')) };
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
function useTheme() {
const ctx = React.useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
return ctx;
}