Продвинутые типы
Продвинутые типы
Технический фундамент композиции типов
Продвинутые типы полезны, когда нужно формально описать сложные состояния:
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-типы ускоряют разработку и уменьшают дублирование типовых описаний.