الـ enum في TypeScript مجموعة مسماة من الثوابت. ينشئ enum Direction { Up, Down, Left, Right } نوعًا هو Direction، وكائنًا وقت التشغيل تصل إلى أعضائه بـ Direction.Up. تُرقَّم الأعضاء بدءًا من 0 ما لم تعطها قيمًا، وتعطي enums النصية كل عضو نصًا مقروءًا.
الـ enums من الميزات القليلة في TypeScript التي ليست مجرد أنواع: يصبح الـ enum كائن JavaScript حقيقيًا عند ترجمة الكود.
Enums الرقمية
دون قيم ابتدائية تحصل الأعضاء على 0 و1 و2 وهكذا. أعطِ العضو الأول رقمًا فتكمل البقية منه. ويمكنك أيضًا ضبط كل قيمة صراحة، وهذا الخيار الآمن عندما تُخزَّن الأرقام في قاعدة بيانات أو تُرسل عبر الشبكة.
الاعتماد على الترقيم التلقائي مناسب للقيم التي لا تغادر البرنامج أبدًا. أما إذا قد يتغير ترتيب الأعضاء والأرقام محفوظة في أي مكان، فإن إدراج عضو في المنتصف يعيد ترقيم كل ما بعده بصمت.
ما الذي يُترجم إليه الـ enum
تُمحى الأنواع، لكن الـ enum لا يُمحى. هذا هو كود JavaScript الذي تنتجه TypeScript لـ enum رقمي وآخر نصي:
enum Direction { Up, Down, Left, Right }
enum Status { Active = "ACTIVE", Inactive = "INACTIVE" }
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
Direction[Direction["Left"] = 2] = "Left";
Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
var Status;
(function (Status) {
Status["Active"] = "ACTIVE";
Status["Inactive"] = "INACTIVE";
})(Status || (Status = {}));
يعيد Direction["Up"] = 0 القيمة 0، لذلك يُضبط Direction[0] = "Up" في الجملة نفسها. وهكذا يربط enum الرقمي في الاتجاهين: من الاسم إلى الرقم ومن الرقم إلى الاسم. هذا هو التعيين العكسي (reverse mapping). أما enums النصية فتربط الأسماء بالقيم فقط.
للكائن Direction المطبوع ثمانية مفاتيح: الأسماء الأربعة والأرقام الأربعة. وهذا يهم بمجرد أن تكرر عليه.
Enums النصية
كل عضو في enum نصي يحتاج إلى قيمة نصية صريحة. تظهر القيم كما هي في السجلات وJSON وقواعد البيانات، ما يجعل enums النصية أسهل في التصحيح من الأرقام.
للـ enum النصي طبيعة اسمية (nominal) بطريقة تفاجئ الناس: لا يمكن إسناد نص عادي إليه، حتى عندما يطابق النص قيمة أحد الأعضاء.
index.ts(7,5): error TS2820: Type '"ACTIVE"' is not assignable to type 'Status'. Did you mean 'Status.Inactive'?
(الاقتراح في الرسالة تخمين من المترجم وهو خاطئ هنا؛ الحل هو Status.Active.) وفي الاتجاه الآخر، يمكن استخدام قيمة Status حيثما يُتوقع string. وعندما تصل القيم كنصوص، من JSON أو نموذج، حوّلها بفحص مثل الموجود في قسم فحص القيم أدناه.
استخدام enum كنوع
اسم الـ enum نوع قيمه هي أعضاؤه. ومع switch تتحقق TypeScript من معالجة كل عضو عندما يجب أن تعيد الدالة قيمة:
إذا أُضيف عضو جديد إلى Shape دون case جديد، يتوقف sides عن الترجمة بالخطأ TS2366، Function lacks ending return statement and return type does not include 'undefined'. وتعرض صفحة switch الفحص الشامل الأكثر صرامة المبني على never.
تُظهر الأسطر الأخيرة ضعفًا حقيقيًا في enums الرقمية. الرقم الحرفي الذي لا يطابق أي عضو، const level: Level = 99، خطأ ترجمة (TS2322)، لكن أي قيمة نوعها number تُقبل، فيمر 57. أما enums النصية فليس فيها هذه الثغرة.
التكرار على enum
الـ enum كائن وقت التشغيل، لذلك تعمل Object.keys وObject.values وObject.entries. في enum النصي تعيد الأعضاء بالضبط. وفي enum الرقمي تعيد أيضًا مدخلات التعيين العكسي، التي تستبعدها:
لتحديد نوع متغير على أنه «أحد أسماء أعضاء الـ enum»، استخدم keyof typeof Direction، وهو union "Up" | "Down" | "Left" | "Right". عندها يبحث Direction[name] عن القيمة بأمان أنواع كامل.
لا يملك enum النصي تعيينًا عكسيًا، لذلك للحصول على اسم عضو من قيمته، ابحث في المدخلات: Object.entries(Status).find(([, v]) => v === "ACTIVE")?.[0] يساوي "Active"، أو undefined عندما لا يملك أي عضو تلك القيمة.
التحقق من أن قيمة موجودة في enum
البيانات القادمة من خارج البرنامج string أو number عادي. يفحصها type guard مقابل قيم الـ enum ويضيّقها إلى نوع الـ enum:
تجنب raw as Status مع المدخلات غير الموثوقة: يُترجم التأكيد، لكن لا شيء يُفحص وقت التشغيل، فينتقل "DELETED" عبر البرنامج بنوع Status صحيح.
const enum
يطلب const enum من المترجم حذف الـ enum وكتابة قيمة كل عضو حيث يُستخدم. لا يوجد كائن وقت التشغيل، فلا يمكن التكرار على شيء ولا التعيين العكسي.
يوفر const enum بضعة بايتات وعملية بحث عن خاصية، لكنه يعتمد على أن يرى المترجم تصريح الـ enum عند ترجمة كل ملف يستخدمه. الأدوات التي تحوّل ملفًا واحدًا في كل مرة، مثل Babel وswc، لا تستطيع رؤية const enum معلن في ملف آخر؛ ويرفض حذف الأنواع في Node الـ const enums مثل أي enum آخر؛ ومع isolatedModules أو verbatimModuleSyntax تبلّغ TypeScript عن الخطأ TS2748 عندما تستخدم const enum من ملف تعريف. معظم كود التطبيقات لا يحتاج إلى const enums.
enum مقابل union type مقابل كائن as const
هناك ثلاث طرق شائعة لتعريف مجموعة ثابتة من القيم:
enum | union من القيم الحرفية | كائن as const | |
|---|---|---|---|
| موجود وقت التشغيل | نعم، كائن | لا | نعم، كائن عادي |
| التكرار على القيم | Object.values (الرقمي: مع تصفية) | لا، لا شيء للتكرار عليه | Object.values |
يقبل "red" عاديًا | لا (enums النصية) | نعم | نعم |
الوصول بالاسم X.Red | نعم | لا | نعم |
| التعيين العكسي | enums الرقمية فقط | لا | لا |
| يعمل مع حذف الأنواع في Node | لا | نعم | نعم |
يسمح به erasableSyntaxOnly | لا | نعم | نعم |
| صياغة إضافية للتعلم | قواعد enum وconst enums | لا شيء | نمط typeof |
تعتمد فرق كثيرة اليوم افتراضيًا على union من النصوص الحرفية، وتنتقل إلى كائن as const عندما تحتاج إلى القيم وقت التشغيل (للتكرار عليها أو لبناء قائمة منسدلة). والأسباب: أنواع union أنواع خالصة تختفي من الناتج؛ وتقبل النصوص العادية التي تسلّمها JSON وواجهات API؛ والـ enums هي الجزء الوحيد من TypeScript اليومية الذي ليس «JavaScript مع أنواع قابلة للمحو».
وقد أصبحت النقطة الأخيرة عملية. يشغّل Node ملفات .ts مباشرة بحذف الأنواع، والـ enum ليس شيئًا يستطيع حذفه:
node status.ts
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
الخيار --experimental-transform-types في Node يجعل الـ enums تعمل، وخيار المترجم erasableSyntaxOnly يبلّغ عن كل enum بالخطأ TS1294، This syntax is not allowed when 'erasableSyntaxOnly' is enabled.، فيستطيع المشروع منعها من البداية. انظر تشغيل TypeScript لمعرفة كيف يعمل حذف الأنواع. لا شيء من هذا يجعل الـ enums خاطئة: الكود المترجم بـ tsc أو بأداة تجميع يشغّلها دون مشكلة، وقاعدة الكود التي تستخدم enums أصلًا لا تكسب كثيرًا من تحويلها.
الأسئلة الشائعة
ما هو enum في TypeScript؟
مجموعة مسماة من الثوابت تكون نوعًا وكائنًا وقت التشغيل معًا: enum Direction { Up, Down } يتيح لك كتابة Direction.Up واستخدام Direction كنوع لمعامل. وعلى خلاف معظم ميزات TypeScript، لا يُمحى الـ enum: يُترجم إلى كائن JavaScript موجود وقت التشغيل.
كيف أكرر على enum في TypeScript؟
في enum النصي يعطي Object.values(MyEnum) القيم ويعطي Object.keys(MyEnum) الأسماء. أما enum الرقمي فيحتوي أيضًا على مدخلات التعيين العكسي ("0": "Up")، فاستبعدها: Object.keys(Direction).filter((k) => isNaN(Number(k))) يعطي الأسماء فقط. ولا يمكن التكرار على const enum، لأنه غير موجود وقت التشغيل.
كيف أحوّل نصًا إلى قيمة enum في TypeScript؟
افحص النص مقابل قيم الـ enum داخل type guard: function isStatus(s: string): s is Status { return (Object.values(Status) as string[]).includes(s); }. بعد الفحص يصبح نوع s هو Status. أما s as Status وحده فيُترجم لكنه لا يفحص شيئًا وقت التشغيل.
هل أستخدم enum أم union type في TypeScript؟
تفضل فرق كثيرة union من النصوص الحرفية (type Status = "active" | "inactive")، أو كائن as const عندما تحتاج أيضًا إلى القيم وقت التشغيل. تُمحى أنواع union كليًا، وتعمل مع حذف الأنواع المدمج في Node والخيار erasableSyntaxOnly، وتقبل نصوصًا عادية مثل "active". والـ enums مناسبة أيضًا، خاصة في قواعد الكود التي تستخدمها أصلًا.
ما الفرق بين enum و const enum؟
يُترجم enum العادي إلى كائن يمكنك التكرار عليه والبحث فيه وقت التشغيل. أما const enum فيُحذف أثناء الترجمة ويُستبدل كل استخدام له بقيمته (Size.Large يصبح 2)، فلا يكلف شيئًا وقت التشغيل لكن لا يمكن التكرار عليه، والأدوات التي تترجم ملفًا واحدًا في كل مرة تقيّده.