В реальном коде часто есть значения из ограниченного набора: статус урока, роль пользователя, режим кнопки, тип уведомления. В JavaScript это обычно строки:
const status = "published";
Проблема начинается, когда где-то появляется "publish", "Published" или "deleted". JavaScript не знает, какие строки разрешены. TypeScript умеет это описывать через union и literal types
enum тоже решает похожую задачу, но в современном TypeScript его не нужно тянуть в каждый случай. Иногда проще и прозрачнее строковый union
- Что получится в конце
- Literal type: строка как точное значение
- Union type: одно из нескольких
- Используем union в объекте
- Проверка через switch
- Union не только для строк
- enum: именованный набор значений
- as const как альтернатива
- Что выбрать новичку
- Частые ошибки
- Мини-задание
- Что может быть еще интересно по этой теме
- Union лучше enum?
- Почему enum остается в JavaScript?
- Можно ли хранить статусы просто строками?
- Что такое narrowing?
- Что почитать дальше по TypeScript
Что получится в конце
Мы опишем статус урока:
type LessonStatus = "draft" | "published" | "archived";
function getStatusLabel(status: LessonStatus): string {
if (status === "draft") {
return "Черновик";
}
if (status === "published") {
return "Опубликовано";
}
return "В архиве";
}
И сравним это с enum:
enum LessonMode {
Draft = "draft",
Published = "published",
Archived = "archived",
}
После урока будет понятно, когда достаточно union, а когда enum может быть оправдан
Literal type: строка как точное значение
Обычный тип string означает любую строку:
let status: string = "draft";
status = "banana";
TypeScript не против, потому что "banana" тоже строка
Literal type означает конкретную строку:
let status: "draft" = "draft";
status = "banana";
Теперь TypeScript ругнется. Переменная может быть только "draft"
Один literal type сам по себе используется не так часто. Гораздо полезнее объединить несколько значений
Union type: одно из нескольких
Union читается как "или":
type LessonStatus = "draft" | "published" | "archived";
Значение LessonStatus может быть "draft", "published" или "archived". Другие строки не подходят
Пример:
type LessonStatus = "draft" | "published" | "archived";
const statusA: LessonStatus = "draft";
const statusB: LessonStatus = "deleted";
Вторая строка должна подсветиться. Для сайта с уроками это очень полезно: статус не расползается по проекту случайными строками
Используем union в объекте
Опишем урок:
type LessonStatus = "draft" | "published" | "archived";
interface Lesson {
id: number;
title: string;
status: LessonStatus;
}
Теперь объект проверяется:
const lesson: Lesson = {
id: 1,
title: "Union в TypeScript",
status: "published",
};
А вот так нельзя:
const lesson: Lesson = {
id: 1,
title: "Union в TypeScript",
status: "publish",
};
Ошибка маленькая, но типичная. TypeScript ловит ее до того, как статус уйдет в фильтр, API или UI
Проверка через switch
Для статусов удобно использовать switch:
type LessonStatus = "draft" | "published" | "archived";
function getStatusLabel(status: LessonStatus): string {
switch (status) {
case "draft":
return "Черновик";
case "published":
return "Опубликовано";
case "archived":
return "В архиве";
}
}
Если потом добавить новый статус, например "review", придется не забыть обновить функцию. Для больших проектов есть приемы с never, которые помогают проверять полноту, но их лучше разбирать в уроке про ошибки и strict
Union не только для строк
Union может объединять разные типы:
type ID = number | string;
function findLesson(id: ID): string {
return `Ищем урок ${id}`;
}
findLesson(10);
findLesson("lesson-10");
Но с такими union нужно быть аккуратнее. Если значение может быть и числом, и строкой, TypeScript позволит делать только то, что безопасно для обоих вариантов, пока ты не уточнишь тип
Например:
function normalizeId(id: number | string): string {
if (typeof id === "number") {
return id.toString();
}
return id.trim();
}
Проверка typeof сужает тип. Внутри первого блока id уже number, во втором — string
enum: именованный набор значений
enum выглядит так:
enum LessonStatusEnum {
Draft = "draft",
Published = "published",
Archived = "archived",
}
const status = LessonStatusEnum.Published;
Плюс enum в том, что значения собраны в одной сущности и к ним обращаются через имя. Это может быть удобно, если команда привыкла к такому стилю или нужно синхронизироваться с существующим кодом
Минус в том, что enum — не просто тип. Это одна из возможностей TypeScript, которая оставляет след в JavaScript после компиляции. Для многих обычных случаев строковый union проще и прозрачнее
as const как альтернатива
Иногда хочется иметь объект со значениями и тип из этих значений:
const LessonStatuses = {
Draft: "draft",
Published: "published",
Archived: "archived",
} as const;
type LessonStatus = typeof LessonStatuses[keyof typeof LessonStatuses];
Для первого урока это выглядит страшнее, чем union. Но в проектах такой прием встречается: есть объект для runtime, и из него выводится тип
Новичку я бы не начинал с as const. Сначала union. Потом enum. Потом as const, когда появится реальная причина
Что выбрать новичку
Для статусов, ролей, режимов и маленьких наборов строк чаще всего достаточно:
type Role = "student" | "mentor" | "admin";
Для кода, где уже принят enum, можно использовать:
enum Role {
Student = "student",
Mentor = "mentor",
Admin = "admin",
}
Для объекта значений, который нужен и в runtime, и в типах, позже можно изучить as const
Частые ошибки
Первая ошибка — ставить string там, где есть конкретный набор значений. Если статус может быть только одним из трех вариантов, лучше описать эти три варианта
Вторая ошибка — использовать enum автоматически везде. В TypeScript это не всегда самый легкий путь
Третья ошибка — забывать сужать union. Если значение number | string, нельзя обращаться с ним как только со строкой без проверки
Четвертая ошибка — держать похожие статусы в разных местах: "published", "publish", "public". Union помогает собрать словарь значений в одном месте
Мини-задание
Опиши роли пользователя:
type UserRole = "student" | "mentor" | "admin";
function canEdit(role: UserRole): boolean {
return role === "mentor" || role === "admin";
}
Потом проверь:
canEdit("student");
canEdit("owner");
owner должен подсветиться. После этого добавь "owner" в UserRole и реши, должен ли он иметь право редактирования
Что может быть еще интересно по этой теме
Union лучше enum?
Не всегда. Union проще для маленьких наборов строк. Enum может быть удобен в командах, где нужен именованный объект значений или уже есть такой стиль
Почему enum остается в JavaScript?
Потому что enum — не только типовая конструкция. В отличие от type, он может создать объект в итоговом JavaScript
Можно ли хранить статусы просто строками?
Можно, но тогда TypeScript не поможет поймать опечатку. Для статусов лучше дать список разрешенных значений
Что такое narrowing?
Это сужение типа после проверки. Например, typeof id === "number" помогает TypeScript понять, что внутри блока id — число
Что почитать дальше по TypeScript
- interface и type в TypeScript: когда что использовать — если нужно описывать объекты со статусами.
- Generics в TypeScript: первый пример без головоломок — следующий шаг к переиспользуемым типам.
- Ошибки TypeScript: any, unknown, never и strict — чтобы разобраться с narrowing и
never. - TypeScript в React: props, state и события — где union удобен для режимов UI.



