Avatar ·

Как создать enum со строковыми значениями в TypeScript?

Привет! Я только начал изучать TypeScript и столкнулся с проблемой при работе с перечислениями (enums). Мне нужно создать enum, который хранит строковые значения для статусов заказа в интернет-магазине: "pending" (в ожидании), "shipped" (отправлен), "delivered" (доставлен) и "cancelled" (отменён). Я попробовал написать такой код: ```typescript enum OrderStatus { Pending = "pending", Shipped = "shipped", Delivered = "delivered", Cancelled = "cancelled" } ``` Вроде бы всё работает, но я не уверен, правильно ли это. Когда я пытаюсь использовать этот enum в функции: ```typescript function updateOrderStatus(status: OrderStatus) { console.log(`Статус заказа изменён на: ${status}`); } updateOrderStatus("shipped"); // Ошибка! ``` TypeScript выдаёт ошибку: `Argument of type '"shipped"' is not assignable to parameter of type 'OrderStatus'`. Почему это происходит? Ведь "shipped" — это одно из значений enum! Как правильно передавать строковые значения в функцию, которая ожидает enum? И есть ли альтернативные способы создания строковых констант без использования enum? Буду благодарен за подробное объяснение с примерами. <<>>

Проблема и её решение

Ваш код с enum написан абсолютно правильно — это стандартный способ создания перечисления со строковыми значениями в TypeScript. Ошибка возникает из-за того, что TypeScript — строго типизированный язык. Он не позволяет передавать произвольные строки в функцию, ожидающую конкретный enum, даже если эта строка совпадает с одним из значений. Enum в TypeScript — это отдельный тип данных, а не просто набор строк.

Чтобы исправить ошибку, нужно передавать именно член enum, а не строку:

function updateOrderStatus(status: OrderStatus) {
  console.log(`Статус заказа изменён на: ${status}`);
}

// ✅ Правильно: передаём член enum
updateOrderStatus(OrderStatus.Shipped); // Выведет: "Статус заказа изменён на: shipped"

// ❌ Неправильно: передаём строку
updateOrderStatus("shipped"); // Ошибка компиляции

Почему так сделано?

Это обеспечивает типобезопасность. Представьте, что вы случайно передали строку "shiped" (с опечаткой). Если бы TypeScript разрешал передавать любые строки, ошибка обнаружилась бы только во время выполнения. С enum вы получаете ошибку ещё на этапе компиляции — это одна из главных фишек TypeScript.

Дополнительные возможности со строковыми enum

Строковые enum поддерживают обратное отображение (reverse mapping), но только если вы используете числовые значения. Для строковых enum обратного отображения нет — это нормально, потому что строки уникальны и читаемы сами по себе.

// Обратное отображение работает только для числовых enum
enum NumericStatus {
  Pending = 0,
  Shipped = 1
}
console.log(NumericStatus[0]); // "Pending" — обратное отображение

// Для строковых enum такого нет
console.log(OrderStatus["shipped"]); // Ошибка! undefined

Альтернативы: const enum и union types

Если вам не нужны все возможности enum (например, обратное отображение или runtime-объект), можно использовать const enum. Он полностью исчезает на этапе компиляции, что уменьшает размер бандла:

const enum OrderStatus {
  Pending = "pending",
  Shipped = "shipped",
  Delivered = "delivered",
  Cancelled = "cancelled"
}

// После компиляции в JavaScript этого кода не будет
// Вместо него подставятся строки напрямую
let status = OrderStatus.Shipped; // В JS превратится в: let status = "shipped";

Ещё один популярный подход — использовать union type со строковыми литералами. Это особенно удобно, если вам не нужен объект enum в runtime:

type OrderStatus = "pending" | "shipped" | "delivered" | "cancelled";

function updateOrderStatus(status: OrderStatus) {
  console.log(`Статус заказа изменён на: ${status}`);
}

// ✅ Работает и с членами типа, и со строками
updateOrderStatus("shipped"); // Теперь ошибки нет!
updateOrderStatus("delivered"); // Тоже работает

// ❌ Ошибка при неверном значении
updateOrderStatus("shippedd"); // Ошибка: Type '"shippedd"' is not assignable to type 'OrderStatus'

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

  • Обычный enum — когда нужно runtime-значение (например, для сериализации в JSON) или обратное отображение.
  • const enum — когда enum используется только внутри проекта и не нужен в runtime (экономит память).
  • Union type — когда нужна просто строгая типизация строк, без дополнительных возможностей enum.

В вашем случае с интернет-магазином я бы рекомендовал union type, если статусы не используются для каких-то сложных манипуляций. Если же вы планируете добавлять методы или вычисляемые свойства, лучше оставить обычный enum.

<<>>

Дополнительный подход: объект-константа с as const

Если вам не нравится синтаксис enum или вы работаете в проекте, где enums запрещены линтером (например, из-за проблем с tree-shaking), можно использовать объект с ключевым словом as const. Это даёт строгую типизацию, как у enum, но без создания отдельного типа:

export const OrderStatus = {
  Pending: "pending",
  Shipped: "shipped",
  Delivered: "delivered",
  Cancelled: "cancelled"
} as const;

// Извлекаем тип значений (ключевой момент!)
type OrderStatus = typeof OrderStatus[keyof typeof OrderStatus];
// Теперь OrderStatus = "pending" | "shipped" | "delivered" | "cancelled"

function updateOrderStatus(status: OrderStatus) {
  console.log(`Статус: ${status}`);
}

// ✅ Работает
updateOrderStatus(OrderStatus.Shipped);
updateOrderStatus("shipped");

// ❌ Ошибка при неверной строке
updateOrderStatus("shippedd"); // Ошибка

Преимущества этого подхода:

  • Tree-shaking: объект легко удаляется сборщиком, если не используется.
  • Гибкость: можно добавлять методы к объекту (например, для валидации).
  • Знакомый синтаксис: для разработчиков, пришедших из JavaScript.

Недостатки:

  • Нужно вручную выводить тип через typeof OrderStatus[keyof typeof OrderStatus].
  • Нет обратного отображения (как и у строковых enum).

Этот подход особенно популярен в современных React-проектах, где предпочитают избегать enum ради совместимости с бамбулом (Babel) и лучшей поддержки tree-shaking.