Перейти к содержимому

61. Enum и Literal types

TypeScript: Enum и Literal Types – Мощь Констант и Точных Значений

Заголовок раздела «TypeScript: Enum и Literal Types – Мощь Констант и Точных Значений»

Иллюстрация к уроку

В мире разработки, где критически важна стабильность и предсказуемость, TypeScript предлагает мощные инструменты для определения строгих, но гибких наборов значений. Enum и Literal Types позволяют нам отойти от “магических строк” и “магических чисел”, вводя типобезопасность на уровне констант.

Проблема без типов: Ограниченные константы и потенциальные ошибки

Заголовок раздела «Проблема без типов: Ограниченные константы и потенциальные ошибки»

Без строгих типов мы часто полагаемся на строковые или числовые константы, определенные через const переменные или напрямую в коде. Это приводит к нескольким проблемам:

  1. Опечатки: Легко допустить ошибку при вводе строки, что приведет к трудноуловимым багам в рантайме.
  2. Отсутствие автодополнения: IDE не всегда может предложить все возможные варианты.
  3. Неявные зависимости: Изменение константы в одном месте может потребовать ручного поиска и обновления всех её использований.

Рассмотрим пример обработки статусов заказа:

// Проблема: Использование "магических строк" без строгих типов
function processOrderStatus(status: string) {
if (status === 'pending') {
console.log('Заказ ожидает обработки...');
} else if (status === 'active') {
console.log('Заказ активен.');
} else if (status === 'complete') {
console.log('Заказ завершен.');
} else {
console.warn(`Неизвестный статус: ${status}`); // <-- Это может случиться из-за опечатки
}
}
processOrderStatus('pending');
processOrderStatus('active');
processOrderStatus('compleated'); // Опечатка! TypeScript не поможет здесь без явных типов.
// Выведет "Неизвестный статус: compleated", что является ошибкой, которую сложно отловить.

Решение с TypeScript: Типобезопасные Enum и Literal Types

Заголовок раздела «Решение с TypeScript: Типобезопасные Enum и Literal Types»

TypeScript предоставляет два основных способа решения этой проблемы, каждый со своими особенностями.

Enum (перечисления) позволяют нам определить набор именованных констант. Они создают реальный объект в JavaScript рантайме, что полезно, если вам нужна возможность итерации или рефлексии.

1. Числовые Enums (Numeric Enums) По умолчанию значения Enum являются числовыми, начиная с 0.

// Решение: Numeric Enum
enum OrderStatus {
PENDING, // 0
ACTIVE, // 1
COMPLETE // 2
}
function processOrderStatusEnum(status: OrderStatus) {
switch (status) {
case OrderStatus.PENDING:
console.log('Заказ ожидает обработки (Enum)...');
break;
case OrderStatus.ACTIVE:
console.log('Заказ активен (Enum).');
break;
case OrderStatus.COMPLETE:
console.log('Заказ завершен (Enum).');
break;
// TypeScript предупредит, если не все варианты OrderStatus обработаны в исчерпывающем switch
}
}
processOrderStatusEnum(OrderStatus.PENDING); // Типобезопасно и с автодополнением
// processOrderStatusEnum('compleated'); // Ошибка TS2345: Аргумент '"compleated"' не может быть присвоен параметру типа 'OrderStatus'.
// processOrderStatusEnum(99); // Ошибка TS2345: Аргумент '99' не может быть присвоен параметру типа 'OrderStatus'.

2. Строковые Enums (String Enums) Строковые Enums более выразительны и обычно предпочтительнее для лучшей читаемости и отладки, хотя и имеют чуть больший “вес” в рантайме.

// String Enum: более выразительны
enum HttpMethod {
GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE"
}
function makeHttpRequest(method: HttpMethod, url: string) {
console.log(`Отправка ${method} запроса на ${url}`);
}
makeHttpRequest(HttpMethod.GET, '/api/users'); // Читабельно и безопасно
// makeHttpRequest("get", '/api/items'); // Ошибка TS2345: Аргумент '"get"' не может быть присвоен параметру типа 'HttpMethod'.

Literal Types позволяют определить тип, который может быть только одним конкретным значением (строкой, числом, булевым). Чаще всего они используются в объединениях (union types), чтобы создать тип, который может быть одним из нескольких точных значений. Literal Types не создают нового объекта в рантайме, что делает их “легковесным” решением.

// Решение: Literal Types для строковых значений
type AllowedStatus = "pending" | "active" | "complete";
function processOrderStatusLiteral(status: AllowedStatus) {
if (status === 'pending') {
console.log('Заказ ожидает обработки (Literal)...');
} else if (status === 'active') {
console.log('Заказ активен (Literal).');
} else if (status === 'complete') {
console.log('Заказ завершен (Literal).');
}
}
processOrderStatusLiteral('pending'); // Автодополнение и типобезопасность
// processOrderStatusLiteral('compleated'); // Ошибка TS2345: Аргумент '"compleated"' не может быть присвоен параметру типа 'AllowedStatus'.
// Literal Types для числовых значений
type ResponseCode = 200 | 400 | 404 | 500;
function handleResponse(code: ResponseCode) {
console.log(`Обработка ответа с кодом: ${code}`);
}
handleResponse(200);
// handleResponse(201); // Ошибка TS2345: Аргумент '201' не может быть присвоен параметру типа 'ResponseCode'.

1. as const для создания Literal Union из объекта Это мощный способ получить Literal Union из значений объекта, используя typeof и индексированные доступы, при этом не создавая enum и сохраняя гибкость объекта.

// Продвинутая техника: `as const`
const AVAILABLE_COLORS = {
RED: 'red',
GREEN: 'green',
BLUE: 'blue'
} as const; // `as const` делает свойства объекта readonly и их значения - literal types
// Тип Color будет "red" | "green" | "blue"
type Color = typeof AVAILABLE_COLORS[keyof typeof AVAILABLE_COLORS];
function selectColor(color: Color) {
console.log(`Выбран цвет: ${color}`);
}
selectColor(AVAILABLE_COLORS.RED); // Доступ к значению по ключу
selectColor('green'); // Прямое использование литерала
// selectColor('yellow'); // Ошибка TS2345: Аргумент '"yellow"' не может быть присвоен параметру типа 'Color'.

2. const enum: “Исчезающие” перечисления const enum — это оптимизированная версия enum, которая полностью удаляется на этапе компиляции, а её использования заменяются на инлайновые значения. Это сокращает размер бандла, но лишает вас объекта в рантайме.

// const enum: компилируется в инлайновые значения, нет объекта в рантайме
const enum LogLevel {
DEBUG,
INFO,
WARN,
ERROR
}
function logMessage(level: LogLevel, message: string) {
if (level === LogLevel.ERROR) {
console.error(`[ERROR] ${message}`);
} else if (level === LogLevel.INFO) {
console.info(`[INFO] ${message}`);
}
}
logMessage(LogLevel.INFO, "Пользователь вошел.");
// В скомпилированном JS это будет выглядеть как:
// if (level === 3 /* LogLevel.ERROR */) { ... } else if (level === 1 /* LogLevel.INFO */) { ... }

Когда что использовать?

  • Enum: Если вам нужен реальный объект в рантайме (например, для итерации по всем значениям, или когда значения должны быть объектами, а не примитивами), или когда вы хотите четко сгруппировать связанные константы в единое сущность.
  • Literal Types (включая as const объекты): Если вам нужна только типобезопасность на этапе компиляции, минимальный “вес” в рантайме, и вы хотите определить строгий набор допустимых значений. Часто предпочтительнее для простых строковых/числовых ограничений из-за меньшего оверхеда.

Практика: Реализация типизированного API-клиента

Заголовок раздела «Практика: Реализация типизированного API-клиента»

Давайте применим полученные знания для создания типобезопасного API-клиента.

// Практика: Типизированный API-клиент
// Используем Enum для известных и фиксированных конечных точек API
enum ApiEndpoint {
USERS = "/api/v1/users",
PRODUCTS = "/api/v1/products",
ORDERS = "/api/v1/orders"
}
// Используем Literal Union для строгих методов HTTP
type HttpMethodStrict = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
interface RequestOptions<T> {
method: HttpMethodStrict;
endpoint: ApiEndpoint;
headers?: Record<string, string>;
data?: T; // Опциональные данные для POST/PUT/PATCH запросов
}
function apiClient<TResponse, TRequest = unknown>(options: RequestOptions<TRequest>): Promise<TResponse> {
console.log(`Отправка ${options.method} запроса на ${options.endpoint}`);
if (options.data) {
console.log('Данные запроса:', options.data);
}
// Здесь была бы реальная логика fetch/axios
return Promise.resolve({} as TResponse); // Заглушка для примера
}
// Пример использования:
interface User { id: string; name: string; email: string; }
interface NewUser { name: string; email: string; }
// Получение списка пользователей
apiClient<User[], undefined>({
method: "GET",
endpoint: ApiEndpoint.USERS
}).then(users => console.log('Получены пользователи:', users));
// Создание нового пользователя
apiClient<User, NewUser>({
method: "POST",
endpoint: ApiEndpoint.USERS,
data: { name: "Alice", email: "[email protected]" }
}).then(newUser => console.log('Создан пользователь:', newUser));
// При попытке использовать несуществующий метод или эндпоинт, TypeScript выдаст ошибку:
// apiClient({ method: "INVALID_METHOD", endpoint: ApiEndpoint.PRODUCTS }); // Ошибка TS2345

Используя Enum и Literal Types, мы создали надежный и предсказуемый интерфейс для нашего API-клиента, значительно снизив риск ошибок, связанных с опечатками или некорректными значениями.