Продвинутые типы

Продвинутые типы

Технический фундамент композиции типов

Продвинутые типы полезны, когда нужно формально описать сложные состояния:

  • union моделирует "один из вариантов";
  • intersection объединяет требования нескольких контрактов;
  • literal/discriminated-подход делает состояния явно различимыми;
  • utility-типы помогают переиспользовать модель без копипаста.

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

Зачем нужны продвинутые типы

Когда проект становится сложнее, базовых string/number уже мало. Данные могут быть в нескольких формах, часть полей опциональна, а логика зависит от конкретного варианта объекта. Продвинутые типы в TypeScript позволяют описывать это явно и безопасно.

Ключевой момент: продвинутые типы помогают делать сложные контракты точными, а не расплывчатыми.

Проверь себя: почему тип any в сложной модели данных быстро приводит к скрытым багам?

Union и narrowing

Union (|) позволяет указать несколько возможных типов.

type Id = string | number;

function normalizeId(id: Id): string {
  return String(id);
}

Чтобы безопасно работать с union, используют narrowing - сужение типа через проверки.

function print(value: string | number) {
  if (typeof value === 'string') {
    console.log(value.toUpperCase());
  } else {
    console.log(value.toFixed(2));
  }
}

Смотри, что важно: без narrowing TypeScript не даст вызвать методы конкретного типа.

Intersection-типы

Intersection (&) объединяет несколько типов в один.

type WithId = { id: string };
type WithTimestamp = { createdAt: string };

type Entity = WithId & WithTimestamp;

Теперь Entity должен иметь поля из обоих типов.

Это полезно для компоновки моделей без дублирования.

Literal-типы и discriminated unions

Literal-тип фиксирует конкретное значение, например 'success'.

type ApiState =
  | { status: 'loading' }
  | { status: 'success'; data: string[] }
  | { status: 'error'; message: string };

По полю status TypeScript может точно сузить тип ветки.

function render(state: ApiState) {
  if (state.status === 'success') {
    return state.data.length;
  }
  return 0;
}

Чтобы не забывать обрабатывать все варианты, часто используют exhaustive checking через never.

function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${JSON.stringify(value)}`);
}

function renderStrict(state: ApiState) {
  switch (state.status) {
    case 'loading':
      return 0;
    case 'success':
      return state.data.length;
    case 'error':
      return state.message;
    default:
      return assertNever(state); // если добавить новый статус, здесь появится ошибка компиляции
  }
}

Проверь себя: почему discriminated union удобнее, чем объект с десятком опциональных полей?

Type alias vs interface

В продвинутых моделях часто используют type, потому что он легко работает с union/intersection.

  • interface удобен для "формы объекта" и расширения;
  • type удобен для композиции и выражений типов.

Оба инструмента важны, выбор зависит от задачи.

Utility-типы

TypeScript дает готовые типовые утилиты:

  • Partial<T> - все поля опциональны;
  • Required<T> - все поля обязательны;
  • Pick<T, K> - взять только часть полей;
  • Omit<T, K> - убрать часть полей.
  • Readonly<T> - сделать поля только для чтения;
  • Record<K, V> - тип для объектов-словарей (ключи K, значения V).
interface User {
  id: string;
  name: string;
  email: string;
}

type UserPreview = Pick<User, 'id' | 'name'>;

Мини-сценарий: для карточки пользователя в списке нужен только краткий тип, без лишних полей.

Частые ошибки новичков

  • Делать слишком широкий union без стратегии narrowing.
  • Путать intersection с объединением значений в рантайме.
  • Злоупотреблять сложными типами там, где хватит простого интерфейса.
  • Использовать as для принудительного приведения вместо корректной проверки.

Анти-провал: если тип становится нечитаемым, остановись и выдели промежуточные alias с понятными именами.

Что будет, если изменить входные данные

Если в ApiState добавить новый статус 'empty', TypeScript подсветит места, где ты не обработал эту ветку. Это помогает не забывать сценарии при расширении продукта и снижает риск "мертвых" состояний UI.

Проверь себя: почему это особенно полезно в больших командах и долгих проектах?

Краткий итог

  • Продвинутые типы нужны для точного описания сложных данных.
  • Union + narrowing делают многовариантные входы безопасными.
  • Intersection помогает собирать составные модели.
  • Discriminated unions отлично работают для состояний интерфейса и API.
  • Utility-типы ускоряют разработку и уменьшают дублирование типовых описаний.