تایپ اسکریپت به دلایل زیادی مورد استقبال جامعه توسعه‌دهندگان قرار گرفته است. یکی از این دلایل، بررسی‌های استاتیکی است که برای کدهای نوشته شده در آن ارائه می‌دهد. تایپ اسکریپت به ما کمک می‌کند تا کدی که داریم قابل پیش‌بینی‌تر و مستندتر شود، refactoring آن آسان‌تر باشد و درنهایت خطاهای احتمالی که ممکن است در زمان اجرا برنامه با آن‌ها مواجه شویم، کاهش پیدا کند. یکی از مکانیسم‌های زبانی که برای تایپ اسکریپت بسیار مهم است enum می‌باشد.

ما در این مقاله قصد داریم تا در مورد اینکه enum چیست، چرا باید آن‌ها را extend کنیم و چگونه باید این کار را انجام دهیم صحبت کنیم. همچنین بهترین روش‌ها برای کار با enum در تایپ اسکریپت را نیز مورد بررسی قرار خواهیم داد.

enum در تایپ اسکریپت چیست؟

enum به توسعه‌دهندگان اجازه می‌دهد تا مجموعه‌ای دقیق از گزینه‌ها را برای یک متغیر تعریف کنند. به عنوان مثال:

enum Door {
  Open,
  Closed,
  Ajar // half open, half closed
}

enumها به‌طور پیش‌فرض بر روی عدد enums تنظیم می‌شوند، بنابراین enum فوق اساساً یک آبجکت با ۰، ۱ و ۲ به عنوان کلید آن است، که می‌توانیم آن را در کد جاوااسکریپت ترجمه شده زیر مشاهده کنیم:

"use strict";
var Door;
(function (Door) {
    Door[Door["Open"] = 0] = "Open";
    Door[Door["Closed"] = 1] = "Closed";
    Door[Door["Ajar"] = 2] = "Ajar"; // half open, half closed
})(Door || (Door = {}));
console.log(Door.FullyOpened);

در تایپ اسکریپت، می‌توانیم از enum رشته‌ای نیز استفاده کنیم، مثلا:

enum Door {
  Open = "open",
  Closed = "closed",
  Ajar = "ajar" // half open, half closed
}

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

اگر سعی کنیم از متغیر دیگری استفاده کنیم، با یک type error مانند خطای زیر مواجه می‌شویم:

enum Door {
  Open = "open",
  Closed = "closed",
  Ajar = "ajar" // half open, half closed
}
console.log(Door.FulyOpened)
Property 'FullyOpened' does not exist on type 'typeof Door'.

چرا باید یک enum را extend کنیم؟

Extension یکی از چهار رکن شی گرایی است و یک ویژگی زبان موجود در تایپ اسکریپت می‌باشد. extend کردن یک enum به ما این امکان را می‌دهد تا یک تعریف متغیر را کپی کنیم و موارد دیگری را به آن اضافه نماییم.

به عنوان مثال، ممکن است سعی کنیم کاری شبیه به مثال زیر انجام دهیم:

enum Door {
  Open = "open",
  Closed = "closed",
  Ajar = "ajar" // half open, half closed
}

enum DoorFrame extends Door { // This will not work!
  Missing = "noDoor"
}

console.log(DoorFrame.Missing)

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

اما باید به این نکته توجه داشته باشیم که چرا قطعه کد بالا به درستی کار نمی‌کند؛ یعنی نمی‌تواند transpile شود و چهار خطای مختلف ایجاد می‌کند.

چگونه می‌توانیم یک enum را در تایپ اسکریپت extend کنیم؟

به طور خلاصه، ما نمی‌توانیم enum را extend کنیم. زیرا، تایپ اسکریپت هیچ ویژگی زبانی برای extend کردن آن‌ها ارائه نمی‌دهد. با این حال، راه‌حل‌هایی وجود دارد که می‌توانیم از آن‌ها برای دستیابی به آنچه که inheritance انجام می‌دهد، استفاده کنیم.

union type در تایپ اسکریپت

enum Door {
  Open = "open",
  Closed = "closed",
  Ajar = "ajar" // half open, half closed
}

enum DoorFrame {
  Missing = "noDoor"
}

type DoorState = Door | DoorFrame; 

let door: DoorState;
door = Door.Ajar
console.log(door) // 'ajar'
door = DoorFrame.Missing
console.log(door) // 'noDoor'

در بلاک کد بالا، از union type استفاده کردیم. این union مانند یک “or” عمل می‌کند، که به سادگی این موضوع را بیان می‌کند که تایپ DoorState یا از تایپ Door و یا از تایپ DoorFrame خواهد بود. این بدان معناست که DoorState می‌تواند از هر یک از متغیرهای دو enum به جای یکدیگر استفاده کند.

با این حال، یک نکته بسیار مهم این است که ما یکی از بزرگ‌ترین مزایای enum را از دست می‌دهیم، یعنی ارجاع به گزینه‌های enum مانند یک ویژگی آبجکت معمولی، مثل DoorState.Open یا DoorState.Missing.

در تایپ اسکریپت، استفاده از مقادیر enum، مانند ajar و noDoor نیز امکان‌پذیر نیست. تنها گزینه ما ارجاع به enumهای منفرد است که مانند DoorFrame.Missing یا Door.Open یک محدودیت می‌باشد.

spread syntax

هنگامی که enum مورد نظر ما در تایپ اسکریپت ترجمه می‌شود، به یک آبجکت جاوااسکریپتی با کلیدها و مقادیری که enum ما مشخص می‌کند تبدیل می‌گردد.

در تایپ اسکریپت، اگر بخواهیم می‌توانیم صرفاً جاوااسکریپت بنویسیم. در واقع، این یک نقطه قوت بزرگ برای تایپ اسکریپت است. برای مثال می‌توانیم نام file.js را به file.ts تغییر دهیم و بررسی‌های کامپایلر را برای کد خود خاموش نماییم. تا زمانی که مراحل کامپایل و یا transpile را اجرا می‌کنیم، بدون تغییر کد همه چیز به خوبی کار می‌کند.

بنابراین با درک این موضوع که وقتی enum ما به صورت واقعی به یک آبجکت تبدیل می‌شود، می‌توانیم آن را مانند یک آبجکت جاوااسکریپت در نظر بگیریم و مانند مثال زیر از spread syntax برای ایجاد یک آبجکت جدید با گزینه‌ها بیشتر استفاده کنیم:

enum Move {
  LEFT = 'Left',
  RIGHT = 'Right',
  FORWARD = 'Forward',
  BACKWARD = 'Backward'
}
const myMove = {
  ...Move,
  JUMP: 'Jump'
}

این راه حل به عنوان راه حل دوم در نظر گرفته شده است، زیرا به خوبی union type نیست. دلیل این اتفاق این است که « ترکیب » enum در زمان اجرا اتفاق می‌افتد، در حالی که وقتی از union type استفاده می‌کنیم، بررسی تایپ می‌تواند در زمان کامپایل و یا transpile رخ دهد، نه در زمان اجرا.

استفاده از as const

در بخش‌های قبلی، استفاده از union type تایپ اسکریپت را به عنوان راهی برای extend کردن enum و محدودیت‌های آن رویکرد بررسی کردیم.

گزینه دیگر این است که از عبارت const assertion در تایپ اسکریپت، یعنی as const استفاده کنیم. برای درک صحیح این گزینه، دو نکته کلیدی وجود دارد که باید آن‌ها را در نظر داشته باشیم:

با استفاده از این ویژگی‌های const assertionها، می‌توانیم literalهای آبجکت read-only را با گزینه‌های enum خود ایجاد و ترکیب کنیم و با این آبجکت تازه تشکیل شده یک تایپ بسازیم. به عنوان مثال:

const Door = {
  Open: "open",
  Closed: "closed",
  Ajar: "ajar" // half open, half closed
} as const

const DoorFrame = {
  Missing: "noDoor"
} as const

const DoorState = {
  ...Door,
  ...DoorFrame
} as const

type DoorState = typeof DoorState[keyof typeof DoorState]

let door: DoorState;
door = DoorState.Open
console.log("Door has a value matching enum object Door", door)

door = DoorState.Missing
console.log("Door has a value matching enum object DoorFrame", door)

گزینه فوق به ما این امکان را می‌دهد که از مزایای enum با استفاده از literalهای آبجکت جاوااسکریپت بهره‌مند شویم.

همچنین می‌توانیم از generic در تایپ اسکریپت برای ایجاد یک تایپ DoorState استفاده کنیم که فقط اجازه انتساب از ویژگی‌های آبجکت DoorState literal را می‌دهد:

door = "apple" // throws Error - Type '"apple"' is not assignable to type 'DoorState'.

door = DoorState["apple"] // sets undefined but throws no errors

بررسی بهترین شیوه‌های enum در تایپ اسکریپت

ما در مورد اینکه چگونه می‌توانیم enum را در تایپ اسکریپت extend کنیم بحث کرده‌ایم، اما باید به این نکته توجه داشته باشیم که enumها یک ترفند جادویی نیستند که بتوانیم از آن‌ها برای رفع همه مشکلات استفاده کنیم. اگر از enum به اشتباه استفاده کنیم، این موضوع می‌تواند به جای بهبود کد، خوانایی، مقیاس‌پذیری و قابلیت نگه‌داری آن را دچار مشکل کند.

بنابراین، در ادامه مقاله قصد داریم تا برخی از بهترین شیوه‌ها و الگوهای رایج برای استفاده هنگام کار با enum در تایپ اسکریپت را با هم بررسی کنیم.

۱ – خودداری از استفاده از heterogenous enum

مشاهده کردیم که چگونه می‌توانیم رشته‌هایی مانند مثال زیر را داشته باشیم:

enum Seasons {
  Summer = "Summer",
  Winter = "Winter",
  Spring = "Spring",
  Fall = "Fall"
}

در کنار enumهای عددی مانند این:

enum Decision {
  Yes,
  No
}

اما نوع سومی از enum وجود دارد که ممکن است از آن مطلع نباشیم، به نام heterogenous enum. این قسمت همان جایی است که می‌توانیم از یک رشته و enumهای عددی در همان enum استفاده کنیم. در ادامه یک مثال از مستندات مربوط به آن را داریم:

enum BooleanLikeHeterogeneousEnum {
  No = 0,
  Yes = "YES",
}

البته توجه به این نکته لازم است، اگرچه انجام چنین کاری امکان‌پذیر است اما مستندات تایپ اسکریپت ما را از انجام این عمل منع می‌کند. توصیه می‌شود به جای ایجاد یک heterogenous enum مانند مثال بالا، کار زیر را انجام دهیم:

۲- “anti-patternenums as configuration

گاهی اوقات، فانکشنالیتی کد ممکن است مجبور شود به یک گزینه enum پایبند باشد، که می‌تواند به سرعت به یک anti-pattern تبدیل شود. به عنوان مثال:

enum Operators {
  Add,
  Subtract
}
function calculate(op: Operators, firstNumber: number, secondNumber: number) {
  switch(op) {
    case Operators.Add: return firstNumber + secondNumber
    case Operators.Subtract: return firstNumber - secondNumber
  }
}

کد بالا نسبتا ساده و ایمن به نظر می‌رسد. زیرا مثال ما در واقع ساده و ایمن می‌باشد. اما در پایگاه‌های کد بزرگ، زمانی که جزئیات پیاده‌سازی را به‌طور دقیق به تایپ‌هایی از این قبیل متصل می‌کنیم، می‌توانیم مشکلاتی را ایجاد کنیم:

اگر نیاز به انجام کاری مانند موارد فوق داریم، یک الگوی ساده‌تر (و فشرده‌‎تر) می‌تواند به این صورت باشد:

const Operators = {

  Add: {
    id: 0,
    apply(firstNumber: number, secondNumber: number) { return firstNumber + secondNumber }
  },

  Subtract: {
    id: 1,
    apply(firstNumber: number, secondNumber: number) { return firstNumber - secondNumber }
  }
}

۳- تایپ‌های داده‌هایی که enumها به بهترین شکل نشان می‌دهند

به طور کلی روشی برای گروه‌بندی انواع مختلف داده‌های مورد استفاده در کد وجود دارد: متغیرهای گسسته یا متغیرهای پیوسته.

متغیرهای گسسته داده‌هایی هستند که بین نمایش‌هایشان فاصله‌های واضحی وجود دارد و فقط چند نمایش دارند. به عنوان مثال:

داده‌های گسسته کاندیدای خوبی برای قرار گرفتن در یک enum هستند و می‌توانند به وضوح کد و قابلیت استفاده مجدد آن کمک کنند. داده‌های پیوسته به داده‌هایی اطلاق می‌شود که شکاف‌ها یا گزینه‌های واضحی ندارند. آن‌ها اغلب مقادیری دارند که می‌توانند دنباله‌ای پیوسته باشند، مانند اعداد. یک مثال خوب از داده های پیوسته، اندازه‌گیری است، مانند وزن یک فرد یا سرعت ماشین؛ زیرا می‌توانند طیف وسیعی از مقادیر را داشته باشند.

داده‌های پیوسته نباید در یک enum استفاده شوند. آیا می‌توانیم یک عدد برای سن تصور کنیم؟

enum Age {
  Zero,
  One,
  Two,
  Three,
  Four,
  Five,
  Six
}

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

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

جمع‌بندی

enum در تایپ اسکریپت روشی قدرتمند برای تعریف و مدیریت مجموعه‌ای از مقادیر مرتبط ارائه می‌دهد، اگرچه با محدودیت‌هایی نیز همراه می‌باشد. ما در این مقاله، تکنیک‌هایی را برای دور زدن این محدودیت‌ها با extend کردن enum با استفاده از union typeها، assertionهای as const و موارد دیگر بررسی کردیم. با استفاده از این روش‌ها، می‌توانیم تایپ قوی را حفظ کنیم و در عین حال فانکشنالیتی enum را extend کنیم.