کار با Enum در تایپ اسکریپت یکی از موضوعات مهم برای توسعه‌دهندگان است. Enumها در برنامه‌نویسی مدرن بسیار پرکاربرد هستند و معمولاً برای مدل‌سازی دسته‌بندی‌هایی مانند حالت‌های چراغ راهنمایی، روزهای هفته یا ماه‌های سال به کار می‌روند.

یکی از مزیت‌های اصلی Enumها این است که امکان نگاشت مجموعه‌ای از مقادیر به اعداد را فراهم می‌کنند. این ویژگی باعث می‌شود مقایسه و استفاده از آن‌ها ساده‌تر شود. در تایپ اسکریپت، Enumها ابتدا به‌عنوان یک نگاشت ساده نام به عدد معرفی شدند، اما به مرور گسترش یافتند و اکنون می‌توانند شامل متدها و پارامترها نیز باشند.

در این مقاله به بررسی روش‌های مختلف پیمایش و کار با Enum در تایپ اسکریپت می‌پردازیم.

مرور سریع

اگر بخواهیم کلیدها یا مقادیر عددی را از یک آبجکت جاوااسکریپت استخراج کنیم، سه روش رایج وجود دارد:

// Option 1: Get numeric keys only (keys as strings, filter those that convert to numbers)
const numericKeys = Object.keys(obj).filter(key => !isNaN(Number(key)));

// Option 2: Get numeric values only
const numericValues = Object.values(obj).filter(value => typeof value === 'number');

// Option 3: Get entries with numeric keys or numeric values (custom filter example)
const numericEntries = Object.entries(obj).filter(
([key, value]) => !isNaN(Number(key)) || typeof value === 'number'
);

چرا باید روی Enumها در تایپ اسکریپت پیمایش کنیم؟

Enumها در تایپ اسکریپت در اصل آبجکت‌های ساده هستند. به عنوان مثال:

enum TrafficLight {
  Green = 1,
  Yellow,
  Red
}

در مثال بالا، مقدار Green برابر با عدد ۱ در نظر گرفته شده است. سایر اعضا به‌طور خودکار مقادیر افزایشی می‌گیرند؛ بنابراین Yellow برابر با ۲ و Red برابر با ۳ خواهد بود.

اگر مقدار اولیه Green = 1 را مشخص نمی‌کردیم، تایپ اسکریپت به‌صورت پیش‌فرض مقدار ۰ را برای اولین عضو انتخاب می‌کرد.

گاهی لازم است روی یک Enum در تایپ اسکریپت پیمایش کنیم؛ مثلاً برای اجرای یک عملیات روی تمام اعضا مانند رندر کردن گزینه‌ها در رابط کاربری یا اعتبارسنجی ورودی‌ها.

با این حال، باید توجه داشته باشیم که Enumها می‌توانند عددی یا رشته‌ای باشند و این تفاوت روی روش پیمایش آن‌ها تأثیر مستقیم دارد.

موارد استفاده و انواع Enumها

پیش از بررسی تکنیک‌های پیمایش، بهتر است بدانیم هر روش برای کدام نوع Enum مناسب‌تر است:

در ادامه، چهار روش متداول پیمایش روی Enumها را بررسی می‌کنیم و مشخص می‌کنیم هر روش برای کدام نوع Enum مناسب‌تر است.

چگونه روی یک Enum در تایپ اسکریپت پیمایش کنیم؟

ما می‌توانیم بسته به نوع Enum و نیاز پروژه، از یکی از روش‌های زیر استفاده کنیم:

استفاده از متدهای آبجکت برای پیمایش در Enumها

ساده‌ترین راه برای کار با Enum در تایپ اسکریپت و پیمایش روی آن، تبدیل Enum به یک آرایه است. برای این کار می‌توان از متدهای درونی Object.keys() و Object.values() استفاده کرد:

نمونه کدی که در ادامه خواهیم دید، نشان می‌دهد چگونه می‌توان کلیدها و مقادیر یک Enum را لیست کرد.

const keys = Object.keys(TrafficLight)

keys.forEach((key, index) => {
        console.log(`${key} has index ${index}`)
})

خروجی این مثال به صورت زیر است:

"1 has index 0"
"2 has index 1"
"3 has index 2"
"Green has index 3"
"Yellow has index 4"
"Red has index 5"

همان‌طور که مشاهده می‌کنیم، کلیدهای عددی در ابتدا ظاهر می‌شوند. دلیل این موضوع آن است که Enumهای عددی یک نگاشت معکوس ایجاد می‌کنند. یعنی تایپ اسکریپت Enum را به آبجکتی کامپایل می‌کند که هم شامل نگاشت مستقیم (key value) و هم شامل نگاشت معکوس (value → key) است.

برای استخراج فقط کلیدهای رشته‌ای، باید کلیدهای عددی را فیلتر کنیم:

const stringKeys = Object
    .keys(TrafficLight)
    .filter((v) => isNaN(Number(v)))

stringKeys.forEach((key, index) => {
    console.log(`${key} has index ${index}`)
})

این کد خروجی زیر را تولید می‌کند:

"Green has index 0"
"Yellow has index 1"
"Red has index 2"

از خروجی بالا می‌توان متوجه شد که index صرفاً جایگاه کلید در آرایه برگشتی از Object.keys() است و ارتباطی با مقدار واقعی Enum ندارد.

به‌طور مشابه، می‌توانیم روی مقادیر Enum نیز پیمایش کنیم:

const values = Object.values(TrafficLight)

values.forEach((value) => {
    console.log(value)
})

این کد هم مقادیر رشته‌ای و هم عددی را برمی‌گرداند:

"Green"
 "Yellow"
 "Red"
1
2
3

اگر تنها به مقادیر عددی نیاز داشته باشیم، کافی است مقادیر رشته‌ای را با یک فیلتر حذف کنیم: .filter((v) => !isNaN(Number(v)))

نکته مهم این است که نیاز به فیلتر کردن فقط در Enumهای عددی وجود دارد. وقتی اعضای Enum به مقادیر رشته‌ای نگاشت شوند، دیگر به بررسی کلیدهای عددی نیازی نیست.

enum TrafficLight {
  Green  = "G",
  Yellow = "Y",
  Red        = "R"
}

Object.keys(TrafficLight).forEach((key, index) => {
    console.log(`${key} has index ${index}`)
})

Object.values(TrafficLight).forEach((value) => {
        console.log(value)
})

خروجی این کد نیز به وضوح تفاوت Enumهای عددی و رشته‌ای را نشان می‌دهد. سه خط اول از حلقه اول forEach و سه خط آخر از حلقه دوم forEach هستند:

"Green has index 0"
"Yellow has index 1"
"Red has index 2"
"G"
"Y"
"R"

بسیاری از برنامه‌نویس‌ها Enumهای رشته‌ای را به دلیل خوانایی بیشتر ترجیح می‌دهند. هرچند امکان تعریف Enumهای ترکیبی (عددی + رشته‌ای) نیز وجود دارد، اما معمولاً توصیه نمی‌شود.

استفاده از متدهای Object.keys() و Object.values() راهی ساده برای پیمایش روی Enumهاست. با این حال، این روش همیشه type-safe باقی نمی‌ماند، چون تایپ اسکریپت کلیدها و مقادیر را به‌صورت رشته یا عدد برمی‌گرداند و تایپ اصلی Enum را حفظ نمی‌کند.

نگاشت معکوس در Enumهای عددی

پیش از آنکه سایر روش‌های کار با Enum در تایپ اسکریپت را بررسی کنیم، باید مفهوم نگاشت معکوس در Enumهای عددی را توضیح دهیم.

نگاشت معکوس قابلیتی است که در آن Enumهای عددی به آبجکت‌هایی کامپایل می‌شوند که شامل هر دو نوع نگاشت «name → value» و «value → name» هستند.

برای مثال، یک Enum عددی ساده به این شکل تعریف می‌شود:

enum Drinks {
 WATER = 1,
 SODA,
 JUICE
}

تایپ اسکریپت این Enum را به آبجکتی مشابه شکل زیر کامپایل می‌کند:

const drinks = {
  '1': 'WATER',
  '2': 'SODA',
  '3': 'JUICE',
  'WATER': '1',
  'SODA': '2',
  'JUICE': '3'
}

این ویژگی امکان دسترسی خوانا به مقادیر Enum را فراهم می‌کند و فرایند دیباگ را ساده‌تر می‌سازد. البته این قابلیت فقط در Enumهای عددی وجود دارد و برای Enumهای رشته‌ای اعمال نمی‌شود.

پیمایش با حلقه‌های for

به‌جای استفاده از متدهای Object.keys() و Object.values()، می‌توانیم از حلقه‌های for برای پیمایش روی کلیدهای Enum استفاده کنیم. در این حالت، با کمک نگاشت معکوس می‌توان مقادیر مرتبط را نیز به‌دست آورد. این روش یکی از رویکردهای مهم در کار با Enum در تایپ اسکریپت محسوب می‌شود.

تایپ اسکریپت سه نوع حلقه for ارائه می‌دهد که شامل for..in و for..of نیز هستند. هر دوی این حلقه‌ها را می‌توانیم برای پیمایش روی Enumها به کار ببریم.

پیمایش با for..in در Enumهای عددی

enum TrafficLight {
        Green,
        Yellow,
        Red
}

for (const tl in TrafficLight) {
        const value = TrafficLight[tl]

        if (typeof value === "string") {
                        console.log(`Value: ${TrafficLight[tl]}`)
        }
}

خروجی کد به شکل زیر خواهد بود:

"Value: Green"
"Value: Yellow"
"Value: Red"

در این مثال، ما مقادیر عددی را فیلتر کردیم تا فقط نام اعضای Enum استخراج شوند. اگر به‌جای نام‌ها قصد داشتیم مقادیر عددی را بگیریم، کافی بود در if شرط فیلتر را تغییر دهیم (مثلاً از typeof value !== "string" استفاده کنیم).

پیمایش با for..in در Enumهای رشته‌ای

enum TrafficLight {
  Green  = "G",
  Yellow = "Y",
  Red    = "R"
}

for (const tl in TrafficLight) {
    console.log(`Value: ${TrafficLight[tl]}`)
}

خروجی به صورت زیر است:

Value: G
Value: Y
Value: R

در Enumهای رشته‌ای لازم نیست کلیدهای عددی را بررسی کنیم، چون نگاشت معکوس ایجاد نمی‌شود. در این حالت پیمایش بسیار ساده‌تر است و می‌توانیم مستقیماً مقادیر رشته‌ای را لاگ کنیم.

پیمایش با for..of در Enumها

حلقه For..of در جاوااسکریپت برای پیمایش روی آبجکت iterable طراحی شده است. بنابراین اگر بخواهیم آن را مستقیماً روی Enum اجرا کنیم، با خطا مواجه می‌شویم:

enum TrafficLight {
  Green  = "G",
  Yellow = "Y",
  Red    = "R"
}

for (const tl of TrafficLight) {
    console.log(`Value: ${tl}`)
}

اگر این بلاک را اجرا کنیم، خطایی مانند زیر دریافت می‌کنیم:

Type 'typeof TrafficLight' is not an array type or a string type

علت خطا این است که Enum ذاتاً یک آبجکت قابل پیمایش نیست. برای حل این مشکل باید Enum را ابتدا به مجموعه‌ای iterable تبدیل کنیم؛ مثلاً با استفاده از Object.keys().

enum TrafficLight {
  Green  = "G",
  Yellow = "Y",
  Red    = "R"
}

for (const tl of Object.keys(TrafficLight)) {
    console.log(`Value: ${TrafficLight[tl]}`)
}

خروجی کد:

Value: G
Value: Y
Value: R

البته، همان‌طور که اشاره شد، در Enumهای عددی به‌خاطر وجود نگاشت معکوس لازم است منطق اضافی برای فیلتر کردن داشته باشیم.

enum TrafficLight {
  Green = 1,
  Yellow,
  Red
}

for (const tl of Object.keys(TrafficLight)) {
    const value = TrafficLight[tl]

    if (typeof value === "string") {
        console.log(`Value: ${TrafficLight[tl]}`)
    }
}

خروجی به شکل زیر خواهد بود:

Value: Green
Value: Yellow
Value: Red

همچنین می‌توانیم از for..of برای پیمایش روی مقادیر Enum استفاده کنیم.

تبدیل Enum به یک آرایه تایپ شده

یکی از مزایای Enum این است که مجموعه‌ای محدود از ثابت‌ها را در اختیار ما قرار می‌دهد. روش‌های مختلفی برای پیمایش وجود دارد که بیشتر بر پایه تبدیل Enum به آرایه‌ای از کلیدها یا مقادیر با استفاده از Object.keys() و Object.values() است. این موضوع درک بهتری از کار با Enum در تایپ اسکریپت به ما می‌دهد.

اما یک مشکل مهم اینجاست: این روش‌ها تایپ قوی Enum را از بین می‌برند، چون خروجی فقط رشته یا عدد برمی‌گرداند. برای رفع این مشکل، می‌توانیم به‌صورت صریح نوع کلیدهای Enum را مشخص کنیم:

enum TrafficLight {
  Green = "G",
  Yellow = "Y",
  Red = "R"
}

function enumKeys<O extends object, K extends keyof O = keyof O>(obj: O): K[] {
        return Object.keys(obj).filter(k => Number.isNaN(k)) as K[]
}

تابع enumKeys نمونه‌ای از این پیاده‌سازی است که کلیدهای Enum را استخراج می‌کند و به شکل یک آرایه تایپ شده برمی‌گرداند. این کار باعث می‌شود خروجی enumKeys(TrafficLight) به‌طور دقیق با نوع کلیدهای Enum هم‌خوانی داشته باشد:

("Green" | "Yellow" | "Red")[]

در اینجا، استفاده از .filter(k => Number.isNaN(k)) کمک می‌کند تا در Enumهای عددی، کلیدهای عددی حاصل از نگاشت معکوس حذف شوند. سپس می‌توانیم روی این آرایه تایپ شده با for..of پیمایش کنیم.

for (const tl of enumKeys(TrafficLight)) {
  const value = TrafficLight[tl]

  if (typeof value === "string") {
      console.log(`Value: ${TrafficLight[tl]}`)
  }
}

نکته: از آنجا که for..in فقط ایندکس‌ها را برمی‌گرداند، برای پیمایش روی آرایه تایپ شده بهتر است از for..of استفاده کنیم.

استفاده از Lodash برای پیمایش روی Enumها در تایپ اسکریپت

Lodash یک کتابخانه محبوب جاوااسکریپت است که مجموعه‌ای از متدهای کاربردی را بر اساس پارادایم برنامه‌نویسی Functional ارائه می‌دهد. این متدها باعث می‌شوند کد ما خواناتر و مختصرتر باشد.

برای نصب Lodash کافی است دستور زیر را اجرا کنیم:

npm install lodash --save

این دستور ماژول Lodash را نصب کرده و به‌طور خودکار فایل package.json را به‌روزرسانی می‌کند.

یکی از قابلیت‌های مفید Lodash متد forIn است که می‌توانیم از آن برای پیمایش روی Enumها در تایپ اسکریپت استفاده کنیم:

import { forIn } from 'lodash'

enum TrafficLight {
      Green = 1,
      Yellow,
      Red,
}

forIn(TrafficLight, (value, key) => console.log(key, value))

این متد روی کلیدها و مقادیر یک آبجکت پیمایش کرده و برای هر جفت (key, value) تابعی را اجرا می‌کند. در واقع می‌توان آن را ترکیبی از Object.keys() و Object.values() در نظر گرفت.

همان‌طور که انتظار داریم، اجرای مثال فوق هم کلیدهای رشته‌ای و هم کلیدهای عددی را برمی‌گرداند:

1 Green
2 Yellow
3 Red
Green 1
Yellow 2
Red 3

مانند قبل، می‌توانیم به‌راحتی کلیدهای رشته‌ای یا عددی را فیلتر کنیم تا دقیقاً به داده‌ای که نیاز داریم دست یابیم:

import { forIn } from 'lodash'

enum TrafficLight {
        Green = 1,
        Yellow,
        Red,
}

forIn(TrafficLight, (value, key) => {
        if (isNaN(Number(key))) {
                  console.log(key, value)
        }
})

خروجی کد به شکل زیر خواهد بود:

Green 1
Yellow 2
Red 3

در اینجا، نوع متغیر key یک string است، درحالی‌که نوع متغیر value از جنس TrafficLight خواهد بود. به همین دلیل، این راهکار تایپ مقادیر Enum را حفظ می‌کند:

import { forIn } from 'lodash'

enum TrafficLight {
    Green  = "G",
    Yellow = "Y",
    Red     = "R"
}

forIn(TrafficLight, (value, key) => {
    if (isNaN(Number(key))) {
              console.log(key, value)
    }
})

همان‌طور که انتظار داریم، مثال فوق خروجی زیر را چاپ خواهد کرد:

Green G
Yellow Y
Red R

تابع کمکی پیشنهادی: ()getEnumKeys

برای ساده‌تر کردن فرآیند استخراج کلیدهای Enum می‌توان یک تابع کمکی قابل استفاده مجدد به نام getEnumKeys() نوشت. این تابع هم برای Enumهای عددی و هم رشته‌ای کاربرد دارد و به‌طور خودکار کلیدهای اضافی ناشی از نگاشت معکوس را حذف می‌کند. خروجی آن یک آرایه تمیز از کلیدهای Enum است که در سراسر پروژه می‌توان از آن استفاده کرد:

/**
* Utility to get the string keys of a TypeScript enum.
* Works with both numeric and string enums.
* @param enumObj The enum object
* @returns Array of enum keys as strings
*/
function getEnumKeys<T extends object>(enumObj: T): (keyof T)[] {
return Object.keys(enumObj).filter(key => isNaN(Number(key))) as (keyof T)[];
}

// Example usage:

enum Colors {
Red = 'RED',
Green = 'GREEN',
Blue = 'BLUE',
}

const keys = getEnumKeys(Colors);
console.log(keys); // Output: ['Red', 'Green', 'Blue']

جمع‌بندی

در این مقاله با روش‌های مختلف پیمایش و کار با Enum در تایپ اسکریپت آشنا شدیم، از جمله:

همچنین یاد گرفتیم که نحوه پیمایش در Enumهای رشته‌ای و عددی متفاوت است و با تعریف توابع کمکی مثل getEnumKeys() می‌توانیم این فرآیند را ساده‌تر و خواناتر کنیم.

در نهایت، این ما هستیم که بر اساس نیاز پروژه، نوع Enum و اهمیت حفظ type-safety در تایپ اسکریپت تصمیم می‌گیریم کدام روش پیمایش مناسب‌تر است.