Перейти к основному содержимому

TypeScript с React

В этой главе показано, как начать проект на React с TypeScript и как типизировать то, с чем вы сталкиваетесь каждый день: пропсы, события, состояние, ссылки (refs), дочерние элементы, загрузку данных и контекст.


Зачем использовать TypeScript с React?

TypeScript улучшает React благодаря:

  • Типобезопасности пропсов, состояния и контекста
  • Лучшему автодополнению и рефакторингу в IDE
  • Раннему обнаружению ошибок — ещё во время написания кода

Примечание: В этой главе предполагается, что вы знаете основы React. Если вы новичок в React, сначала пройдите учебник по React.


Начало работы

Создайте новое приложение React + TypeScript с помощью Vite, выполнив эти команды:

  • npm create vite@latest my-app -- --template react-ts
  • cd my-app
  • npm install
  • npm 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; компонент выводит строку с возрастом только тогда, когда он передан.

script.ts
// 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> дают типизированный доступ к событию и его цели.

script.ts
// 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, — у каждого явный аргумент типа.

script.ts
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() проверяется компилятором.

script.ts
function FocusInput() {
   const inputRef = React.useRef<HTMLInputElement>(null);
   return <input ref={inputRef} onFocus={() => inputRef.current?.select()} />;
 }

Типизация дочерних элементов

children — необязательное поле с типом React.ReactNode, поэтому принимается всё, что React умеет отображать.

script.ts
 type CardProps = { title: string; children?: React.ReactNode };
 function Card({ title, children }: CardProps) {
   return (
     <div>
       <h2>{title}</h2>
       {children}
     </div>
   );
 }

Дженерик-функция для загрузки данных

fetchJson<T> возвращает Promise<T>, поэтому форму ответа определяет вызывающий код.

script.ts
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 выбрасывает ошибку, если используется вне провайдера.

script.ts
 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;
 }