Back

TSX и развитие типизированных фронтенд-компонентов

TSX и развитие типизированных фронтенд-компонентов

Вы создаёте React-компонент. Передаёте строку там, где должно быть число. TypeScript перехватывает ошибку раньше, чем браузер. Это простое взаимодействие — типы, предотвращающие ошибки во время выполнения — объясняет, почему TSX стал стандартным форматом для современной фронтенд-разработки.

Эта статья рассматривает, что на практике означают типизированные фронтенд-компоненты: типизация props, обработка событий, паттерны children и как вывод типов TypeScript устраняет шаблонный код. Мы также рассмотрим, как разделение на серверные и клиентские компоненты в React 19 влияет на вашу стратегию типизации.

Ключевые выводы

  • Типизированные TSX-компоненты выявляют несоответствия типов на этапе компиляции, сокращая количество ошибок времени выполнения до того, как код попадёт в браузер.
  • Вывод типов TypeScript автоматически обрабатывает большинство случаев типизации событий — явные аннотации нужны только при выносе обработчиков в отдельные функции.
  • Размеченные объединения (discriminated unions) позволяют создавать типобезопасные компоненты с взаимоисключающим поведением, где TypeScript сужает типы на основе свойства-дискриминанта.
  • Граница между сервером и клиентом в React 19 вводит ограничения сериализации: props, передаваемые от серверных к клиентским компонентам, должны быть сериализуемыми (без функций, классов, символов и других несериализуемых значений).

Что на самом деле означают типизированные TSX-компоненты

Типизированный компонент — это не просто компонент с типизированными props. Это компонент, где TypeScript понимает весь контракт: что входит, что выходит и что происходит между ними.

С современным JSX-преобразованием (react-jsx) вам не нужно импортировать React в каждом файле. Вы пишете TSX, а инструментарий делает всё остальное. Vite настраивает это правильно из коробки:

interface ButtonProps {
  label: string
  disabled?: boolean
}

function Button({ label, disabled = false }: ButtonProps) {
  return <button disabled={disabled}>{label}</button>
}

Обратите внимание, здесь нет React.FC. Этот паттерн опционален — и часто не нужен. Типизация props напрямую в параметре функции чище и избегает неявного prop children, который React.FC добавляет независимо от того, нужен он вам или нет.

Типизация Props и типобезопасность React TSX

Типизация props — это то, с чего начинают большинство разработчиков, но типобезопасность React TSX идёт глубже. Рассмотрим размеченные объединения для компонентов с взаимоисключающим поведением:

type LinkButtonProps = 
  | { variant: 'button'; onClick: () => void }
  | { variant: 'link'; href: string }

function LinkButton(props: LinkButtonProps) {
  if (props.variant === 'link') {
    return <a href={props.href}>Navigate</a>
  }
  return <button onClick={props.onClick}>Click</button>
}

TypeScript сужает тип на основе variant. Вы не можете случайно обратиться к href, когда variant равен 'button'. Этот паттерн хорошо масштабируется для сложных API компонентов.

Типизация событий без шаблонного кода

Обработчики событий часто вызывают затруднения у разработчиков, новых в TypeScript. Хорошая новость: вывод типов автоматически обрабатывает большинство случаев.

import { useState } from 'react'

function SearchInput() {
  const [query, setQuery] = useState('')

  // TypeScript выводит тип события из контекста
  return (
    <input 
      value={query} 
      onChange={(e) => setQuery(e.currentTarget.value)} 
    />
  )
}

Когда вы выносите обработчики в отдельные функции, вам потребуются явные типы:

import type { ChangeEvent } from 'react'

function handleChange(event: ChangeEvent<HTMLInputElement>) {
  // event.currentTarget.value корректно типизирован как string
}

Типизация Children: ReactNode vs ReactElement

Для children React.ReactNode покрывает общий случай — всё, что можно отрендерить:

import type { ReactNode } from 'react'

interface CardProps {
  children: ReactNode
}

Используйте React.ReactElement, когда вам нужны строго JSX-элементы, исключая строки и числа. Для компонентов, принимающих children, PropsWithChildren — более чистая альтернатива ручному добавлению prop children:

import type { PropsWithChildren } from 'react'

interface CardProps {
  title: string
}

function Card({ title, children }: PropsWithChildren<CardProps>) {
  return (
    <div>
      <h2>{title}</h2>
      {children}
    </div>
  )
}

Типизация серверных и клиентских компонентов в React 19

Проекты на React 19 с TypeScript, особенно использующие Next.js App Router, разделяют компоненты на серверную и клиентскую границы. Это влияет на типизацию.

Серверные компоненты могут быть асинхронными и получать данные напрямую:

// Серверный компонент - директива не нужна (по умолчанию в App Router)
async function UserProfile({ userId }: { userId: string }) {
  const user = await fetchUser(userId)
  return <div>{user.name}</div>
}

Клиентские компоненты требуют директивы 'use client' и могут использовать хуки:

'use client'

import { useState } from 'react'

interface CounterProps {
  initialCount: number
}

function Counter({ initialCount }: CounterProps) {
  const [count, setCount] = useState(initialCount)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

Граница типизации имеет значение: props, передаваемые от серверных к клиентским компонентам, должны быть сериализуемыми. Функции, классы, символы и несериализуемые значения (такие как экземпляры классов или Date) не пересекут эту границу.

Form Actions и современные типизированные рабочие процессы

React 19 вводит серверные действия (обычно используемые с формами) как типизированный паттерн для обработки отправок, обычно определяемый в серверных файлах и вызываемый из клиентских компонентов:

async function submitForm(formData: FormData) {
  'use server'
  const email = formData.get('email')
  if (typeof email !== 'string') {
    throw new Error('Email is required')
  }
  // Обработка отправки
}

Это интегрируется с типизированными фронтенд-компонентами, сохраняя обработку форм типобезопасной от клиента до сервера.

Заключение

Типизированные TSX-компоненты сокращают количество ошибок, выявляя несоответствия на этапе компиляции. Вывод типов TypeScript минимизирует шаблонный код — вам редко нужны явные аннотации типов для хуков или встроенных обработчиков. Разделение на серверные и клиентские компоненты в React 19 добавляет новое соображение: границы сериализации.

Откажитесь от React.FC, если у вас нет конкретной причины. Типизируйте props напрямую. Позвольте выводу типов работать. Ваш редактор отблагодарит вас точным автодополнением и немедленной обратной связью об ошибках.

Часто задаваемые вопросы

Типизируйте props напрямую в параметрах функции. React.FC добавляет неявный prop children независимо от того, нужен он вам или нет, а типизация props напрямую чище. Команда React не рекомендует React.FC как паттерн по умолчанию. Используйте его только когда вам конкретно нужно его поведение, например, при работе с legacy-кодовыми базами, которые этого ожидают.

Для встроенных обработчиков TypeScript автоматически выводит тип события из контекста. Когда вы выносите обработчики в отдельные функции, используйте явные типы, такие как React.ChangeEvent для изменений input или React.MouseEvent для кликов. Обобщённый параметр указывает тип элемента, например HTMLInputElement или HTMLButtonElement.

ReactNode принимает всё, что можно отрендерить, включая строки, числа, null, undefined и JSX-элементы. ReactElement принимает только JSX-элементы, исключая примитивы. Используйте ReactNode в большинстве случаев. Используйте ReactElement, когда вашему компоненту конкретно требуются JSX-дочерние элементы и он должен отклонять простой текст или числа.

Нет. Props, передаваемые от серверных к клиентским компонентам, должны быть сериализуемыми. Функции, классы, символы и другие несериализуемые значения не могут пересечь границу сервер-клиент. Определяйте обработчики событий и коллбэки внутри клиентских компонентов или используйте серверные действия для отправки форм, требующих обработки на стороне сервера.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay