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

Как в интерфейсе указать Union?

2.0 Middle🔥 191 комментариев
#TypeScript

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

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

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

TypeScript Union в интерфейсах: правильный синтаксис

Отличный вопрос о работе с Union types в TypeScript интерфейсах.

Базовый синтаксис Union

Union тип (объединение) описывает значение, которое может быть ОДНОГО из нескольких типов:

// Union из примитивов
type Status = 'loading' | 'success' | 'error';

// Union из типов
type Result = string | number | boolean;

// Union из объектов
type Response = SuccessResponse | ErrorResponse;

Union в интерфейсах

Способ 1: Поле с Union типом

interface User {
  id: string;
  name: string;
  status: 'active' | 'inactive' | 'banned';
  age: number | null;  // number или null
  permissions: string[];
}

Способ 2: Union из интерфейсов

interface SuccessResponse {
  status: 'success';
  data: unknown;
  code: 200;
}

interface ErrorResponse {
  status: 'error';
  message: string;
  code: 400 | 404 | 500;
}

type ApiResponse = SuccessResponse | ErrorResponse;

Теперь ApiResponse может быть либо успешный, либо ошибочный ответ.

Discriminated Union (Дискриминированное объединение)

Это паттерн, где одно поле (discriminator) определяет тип:

interface LoadingState {
  status: 'loading';
  progress: number;
}

interface SuccessState {
  status: 'success';
  data: any;
}

interface ErrorState {
  status: 'error';
  error: Error;
}

type AsyncState = LoadingState | SuccessState | ErrorState;

// Использование
function handleState(state: AsyncState) {
  if (state.status === 'loading') {
    console.log('Загруз:', state.progress);  // ТС знает про progress
  } else if (state.status === 'success') {
    console.log('Успех:', state.data);  // ТС знает про data
  } else if (state.status === 'error') {
    console.log('Ошибка:', state.error);  // ТС знает про error
  }
}

Это мощнее обычного Union, потому что TypeScript может сузить тип по discriminator.

Union в параметрах компонента

interface ButtonProps {
  variant: 'primary' | 'secondary' | 'danger';
  size: 'sm' | 'md' | 'lg';
  onClick: () => void;
}

interface LinkProps {
  href: string;
  target: '_blank' | '_self';
  external: boolean;
}

// Компонент может быть или кнопка, или ссылка
type ActionProps = ButtonProps | LinkProps;

// Но это неудобно, нужен discriminator
type ActionProps = 
  | (ButtonProps & { type: 'button' })
  | (LinkProps & { type: 'link' });

Практические примеры

1. Платежная система

interface CreditCardPayment {
  method: 'credit_card';
  cardNumber: string;
  cvv: string;
  expiryDate: string;
}

interface PayPalPayment {
  method: 'paypal';
  email: string;
  token: string;
}

interface CryptoPayment {
  method: 'crypto';
  walletAddress: string;
  coinType: 'bitcoin' | 'ethereum';
}

type PaymentMethod = CreditCardPayment | PayPalPayment | CryptoPayment;

function processPayment(payment: PaymentMethod) {
  switch (payment.method) {
    case 'credit_card':
      console.log('Обработка карты:', payment.cardNumber);
      break;
    case 'paypal':
      console.log('PayPal:', payment.email);
      break;
    case 'crypto':
      console.log('Крипто:', payment.coinType);
      break;
  }
}

2. Форма с разными типами полей

interface TextField {
  type: 'text';
  label: string;
  placeholder: string;
  maxLength: number;
}

interface SelectField {
  type: 'select';
  label: string;
  options: { value: string; label: string }[];
  multiple: boolean;
}

interface CheckboxField {
  type: 'checkbox';
  label: string;
  checked: boolean;
}

type FormField = TextField | SelectField | CheckboxField;

function renderField(field: FormField) {
  switch (field.type) {
    case 'text':
      return <input placeholder={field.placeholder} maxLength={field.maxLength} />;
    case 'select':
      return <select multiple={field.multiple}>{field.options.map(...)}</select>;
    case 'checkbox':
      return <input type="checkbox" checked={field.checked} />;
  }
}

3. Асинхронное состояние

type LoadingState = {
  state: 'loading';
};

type SuccessState<T> = {
  state: 'success';
  data: T;
};

type ErrorState = {
  state: 'error';
  error: string;
};

type AsyncResult<T> = LoadingState | SuccessState<T> | ErrorState;

// Использование
function useAsync<T>(fn: () => Promise<T>): AsyncResult<T> {
  const [result, setResult] = useState<AsyncResult<T>>({ state: 'loading' });
  
  useEffect(() => {
    fn()
      .then(data => setResult({ state: 'success', data }))
      .catch(error => setResult({ state: 'error', error: error.message }));
  }, [fn]);
  
  return result;
}

Сужение типа (Type Narrowing)

ТипScript умеет сужать Union автоматически:

type Value = string | number;

function process(val: Value) {
  // val: string | number
  
  if (typeof val === 'string') {
    // val: string
    console.log(val.toUpperCase());
  } else {
    // val: number
    console.log(val.toFixed(2));
  }
}

Union с Generics

type Either<L, R> = 
  | { type: 'left'; value: L }
  | { type: 'right'; value: R };

const success: Either<Error, number> = { type: 'right', value: 42 };
const error: Either<Error, number> = { type: 'left', value: new Error('Fail') };

function extract<L, R>(either: Either<L, R>): L | R {
  if (either.type === 'left') return either.value;
  return either.value;
}

Частые ошибки

// Ошибка 1: Забыли discriminator
type Bad = 
  | { id: string; name: string }
  | { id: string; email: string };
// Непонятно какой тип, сложно работать

// Правильно:
type Good = 
  | { type: 'user'; id: string; name: string }
  | { type: 'admin'; id: string; email: string };

// Ошибка 2: Слишком широкий Union
type Bad = string | number | boolean | object | null | undefined;
// Лучше использовать unknown с type guards

// Ошибка 3: Union где нужен Array
type Bad = string | string | string;  // Лучше string[]

Вывод

Union типы в интерфейсах:

  1. Для простых значений: status: 'loading' | 'success' | 'error'
  2. Для сложных: Discriminated Union с уникальным полем
  3. С Generics: для гибкости, как Either<L, R>
  4. Всегда используй type guards для сужения типов

Discriminated Union - это gold standard в TypeScript, используй его везде где есть Union из нескольких интерфейсов.