← Назад к вопросам

Как можно типизировать сложные props когда компонент принимает сложные типы данных?

2.3 Middle🔥 152 комментариев
#TypeScript#React

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Типизация сложных Props в React/TypeScript

При работе с компонентами которые принимают сложные типы данных, важно правильно определить типы props. Это улучшает type safety, документацию кода и developer experience.

Проблема: Без типизации

// Плохо - никакой типизации
function UserProfile(props) {
  // Неясно какие поля есть в props
  return (
    <div>
      <h1>{props.name}</h1>
      <p>{props.email}</p>
      {/* Может быть ошибка на проде */}
    </div>
  );
}

// Или с any
function UserProfile(props: any) {
  // IDE не поможет с автодополнением
  // Нет проверки типов
  return <div>{props.unknownField}</div>;
}

Базовая типизация

interface User {
  id: string;
  name: string;
  email: string;
  age: number;
  avatar?: string;
}

interface UserProfileProps {
  user: User;
  onUpdate?: (user: User) => void;
  loading?: boolean;
}

function UserProfile({ user, onUpdate, loading }: UserProfileProps) {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <button onClick={() => onUpdate?.(user)} disabled={loading}>
        {loading ? 'Saving...' : 'Update'}
      </button>
    </div>
  );
}

Типизация массивов сложных объектов

Массив объектов с вложенными данными:

interface Comment {
  id: string;
  text: string;
  author: User; // Вложенный объект
  timestamp: Date;
  replies: Comment[]; // Рекурсивный тип
}

interface CommentListProps {
  comments: Comment[];
  onReply: (parentId: string, text: string) => Promise<void>;
}

function CommentList({ comments, onReply }: CommentListProps) {
  return (
    <div>
      {comments.map(comment => (
        <div key={comment.id}>
          <p>{comment.text}</p>
          <span>By {comment.author.name}</span>
          {comment.replies.length > 0 && (
            <CommentList comments={comment.replies} onReply={onReply} />
          )}
        </div>
      ))}
    </div>
  );
}

Типизация функций (Callbacks)

interface Product {
  id: string;
  name: string;
  price: number;
}

interface ProductFormProps {
  initialProduct?: Product;
  // Функция которая получает Product и ничего не возвращает
  onSave: (product: Product) => void;
  // Функция которая возвращает Promise
  onDelete: (productId: string) => Promise<void>;
  // Функция с опциональным параметром
  onCancel?: () => void;
}

function ProductForm({ 
  initialProduct, 
  onSave, 
  onDelete, 
  onCancel 
}: ProductFormProps) {
  const [product, setProduct] = useState<Product>(initialProduct || {
    id: '',
    name: '',
    price: 0,
  });

  const handleSave = async () => {
    onSave(product);
  };

  const handleDelete = async () => {
    await onDelete(product.id);
  };

  return (
    <form>
      {/* форма */}
      <button onClick={handleSave}>Save</button>
      <button onClick={handleDelete}>Delete</button>
      {onCancel && <button onClick={onCancel}>Cancel</button>}
    </form>
  );
}

Типизация с обобщенными типами (Generics)

Когда компонент работает с разными типами данных:

// Параметризированный компонент - TableProps<T>
interface TableProps<T> {
  data: T[];
  columns: Array<{
    key: keyof T; // Ключ из типа T
    label: string;
    render?: (value: T[keyof T]) => React.ReactNode;
  }>;
  onRowClick?: (row: T) => void;
}

function Table<T extends { id: string }>({ 
  data, 
  columns, 
  onRowClick 
}: TableProps<T>) {
  return (
    <table>
      <thead>
        <tr>
          {columns.map(col => <th key={col.key as string}>{col.label}</th>)}
        </tr>
      </thead>
      <tbody>
        {data.map(row => (
          <tr key={row.id} onClick={() => onRowClick?.(row)}>
            {columns.map(col => (
              <td key={col.key as string}>
                {col.render ? col.render(row[col.key]) : (row[col.key] as React.ReactNode)}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

// Использование с конкретным типом
interface User {
  id: string;
  name: string;
  email: string;
}

const userColumns: TableProps<User>['columns'] = [
  { key: 'name', label: 'Name' },
  { key: 'email', label: 'Email', render: (email) => <a href={`mailto:${email}`}>{email}</a> },
];

<Table<User> 
  data={users} 
  columns={userColumns} 
  onRowClick={(user) => console.log(user.id)} 
/>

Типизация Union Types

Когда props могут быть разными вариантами:

type NotificationProps = 
  | { type: 'success'; message: string }
  | { type: 'error'; message: string; retryable: boolean }
  | { type: 'warning'; message: string; icon: React.ReactNode }
  | { type: 'info'; message: string };

function Notification(props: NotificationProps) {
  switch (props.type) {
    case 'success':
      // IDE знает что есть только type и message
      return <div className="success">{props.message}</div>;
    
    case 'error':
      // IDE знает что есть retryable
      return (
        <div className="error">
          {props.message}
          {props.retryable && <button>Retry</button>}
        </div>
      );
    
    case 'warning':
      // IDE знает что есть icon
      return <div className="warning">{props.icon} {props.message}</div>;
    
    case 'info':
      return <div className="info">{props.message}</div>;
  }
}

// Использование - TypeScript проверит что нужно передать
<Notification type="error" message="Error" retryable={true} />
<Notification type="info" message="Info" /> // Error - retryable required for 'error'

Типизация с Discriminated Union

Для более сложных случаев:

type FormField = 
  | { kind: 'text'; name: string; value: string; maxLength?: number }
  | { kind: 'number'; name: string; value: number; min?: number; max?: number }
  | { kind: 'checkbox'; name: string; value: boolean }
  | { kind: 'select'; name: string; value: string; options: string[] };

interface FormProps {
  fields: FormField[];
  onChangeField: (fieldName: string, newValue: any) => void;
}

function Form({ fields, onChangeField }: FormProps) {
  return (
    <form>
      {fields.map(field => {
        // Discriminator - 'kind' поле узко определяет тип
        switch (field.kind) {
          case 'text':
            return (
              <input
                key={field.name}
                type="text"
                value={field.value}
                maxLength={field.maxLength}
                onChange={(e) => onChangeField(field.name, e.target.value)}
              />
            );
          
          case 'number':
            return (
              <input
                key={field.name}
                type="number"
                value={field.value}
                min={field.min}
                max={field.max}
                onChange={(e) => onChangeField(field.name, Number(e.target.value))}
              />
            );
          
          case 'checkbox':
            return (
              <input
                key={field.name}
                type="checkbox"
                checked={field.value}
                onChange={(e) => onChangeField(field.name, e.target.checked)}
              />
            );
          
          case 'select':
            return (
              <select
                key={field.name}
                value={field.value}
                onChange={(e) => onChangeField(field.name, e.target.value)}
              >
                {field.options.map(opt => (
                  <option key={opt} value={opt}>{opt}</option>
                ))}
              </select>
            );
        }
      })}
    </form>
  );
}

Типизация Render Props

Когда компонент передает данные в функцию:

interface LoaderProps<T> {
  // Функция которая получает данные и возвращает ReactNode
  children: (data: T, loading: boolean, error?: Error) => React.ReactNode;
  loader: () => Promise<T>;
}

function Loader<T>({ children, loader }: LoaderProps<T>) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error>();

  useEffect(() => {
    setLoading(true);
    loader()
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [loader]);

  if (data === null && loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return <>{children(data as T, loading, error)}</>;
}

// Использование
<Loader<User>
  loader={() => fetch('/api/user').then(r => r.json())}
>
  {(user, loading) => (
    <div>
      <h1>{user.name}</h1>
      {loading && <p>Updating...</p>}
    </div>
  )}
</Loader>

Типизация HOC (Higher Order Components)

interface WithAuthProps {
  user: User;
  isAuthenticated: boolean;
}

function withAuth<P extends WithAuthProps>(
  Component: React.ComponentType<P>
): React.FC<Omit<P, keyof WithAuthProps>> {
  return (props) => {
    const [user, setUser] = useState<User | null>(null);
    
    useEffect(() => {
      fetchUser().then(setUser);
    }, []);

    if (!user) return <div>Loading...</div>;

    return (
      <Component
        {...(props as P)}
        user={user}
        isAuthenticated={true}
      />
    );
  };
}

// Использование
interface MyComponentProps extends WithAuthProps {
  title: string;
}

function MyComponent({ user, isAuthenticated, title }: MyComponentProps) {
  return <div>{title} - {user.name}</div>;
}

const MyComponentWithAuth = withAuth(MyComponent);
// Теперь MyComponentWithAuth требует только title
<MyComponentWithAuth title="My Page" />

Best Practices

  1. Используй Interface для props - точнее и удобнее
  2. Разделяй обязательные и опциональные props - ? для опциональных
  3. Используй Generics для переиспользуемых компонентов - Table, List, Modal
  4. Discriminated Union для вариантов - более типобезопасно чем enum
  5. Экспортируй типы props - чтобы другие компоненты могли их использовать
// Экспортируй типы
export interface ButtonProps {
  variant: 'primary' | 'secondary';
  children: React.ReactNode;
}

export function Button(props: ButtonProps) { }

Вывод

Правильная типизация props - это основа надежного React приложения. TypeScript предоставляет мощные инструменты (Generics, Union Types, Discriminated Union) для типизации любого уровня сложности. Затрати время на типы - сэкономишь время на отладку.

Как можно типизировать сложные props когда компонент принимает сложные типы данных? | PrepBro