Menu

Enum ב-TypeScript: מספריים, מחרוזות ו-const enums

enum ב-TypeScript הוא קבוצה של קבועים עם שמות, כמו enum Direction { Up, Down }. כאן תלמדו על enums מספריים ו-enums של מחרוזות, ל-JavaScript שאליו enum מתקמפל, על מיפוי הפוך, מעבר על enum, const enums, ומתי union של ליטרלים של מחרוזות או אובייקט as const הם הבחירה הטובה יותר.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

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 של מחרוזות:

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 של מחרוזות הוא נומינלי במובן אחד שמפתיע אנשים: מחרוזת רגילה לא ניתנת להשמה אליו, גם כשהטקסט זהה לערך של איבר.

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 על קלט לא אמין: ה-assertion עובר קומפילציה, אבל שום דבר לא נבדק בזמן ריצה, ולכן "DELETED" היה עובר בתוכנית עם הטיפוס של Status תקין.

const Enums

const enum מבקש מהקומפיילר למחוק את ה-enum ולכתוב את הערך של כל איבר במקום שבו משתמשים בו. אין אובייקט בזמן ריצה, ולכן אי אפשר לעבור עליו או להשתמש במיפוי הפוך.

const enum חוסך כמה בתים וחיפוש מאפיין אחד, אבל הוא תלוי בכך שהקומפיילר רואה את ההצהרה של ה-enum כשהוא מקמפל כל קובץ שמשתמש בו. כלים שמתרגמים קובץ אחד בכל פעם, כמו Babel ו-swc, לא יכולים לראות const enum שהוצהר בקובץ אחר; ה-type stripping של Node דוחה const enums כמו כל enum אחר; ועם isolatedModules או verbatimModuleSyntax, TypeScript מדווחת על השגיאה TS2748 כשמשתמשים ב-const enum מקובץ הצהרה. רוב הקוד של אפליקציות לא צריך const enums.

enum מול union type מול אובייקט as const

יש שלוש דרכים נפוצות להגדיר קבוצה קבועה של ערכים:

enumUnion של ליטרליםאובייקט as const
קיים בזמן ריצהכן, אובייקטלאכן, אובייקט רגיל
מעבר על הערכיםObject.values (מספרי: לסנן)לא, אין על מה לעבורObject.values
מקבל "red" רגיללא (enums של מחרוזות)כןכן
גישה בשם X.Redכןלאכן
מיפוי הפוךרק enums מספרייםלאלא
רץ עם type stripping של Nodeלאכןכן
מותר תחת erasableSyntaxOnlyלאכןכן
תחביר נוסף ללמודכללי enum, const enumsאיןהדפוס של typeof

צוותים רבים עוברים היום כברירת מחדל ל-union של ליטרלים של מחרוזות, ומחליפים לאובייקט as const כשהם צריכים את הערכים בזמן ריצה (כדי לעבור עליהם או לבנות רשימה נפתחת). הסיבות: unions הם טיפוסים טהורים ונעלמים מהפלט; הם מקבלים את המחרוזות הרגילות ש-JSON ו-APIs מספקים; ו-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 כדי להבין איך type stripping עובד. שום דבר מזה לא הופך enums לשגויים: קוד שמקומפל עם tsc או עם bundler מריץ אותם בלי בעיה, ובסיס קוד שכבר משתמש ב-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 כשצריך את הערכים גם בזמן ריצה. unions נמחקים לגמרי, עובדים עם ה-type stripping המובנה של Node ועם האפשרות erasableSyntaxOnly, ומקבלים מחרוזות רגילות כמו "active". גם enums בסדר, במיוחד בבסיסי קוד שכבר משתמשים בהם.

מה ההבדל בין enum ל-const enum?

enum רגיל מתקמפל לאובייקט שאפשר לעבור עליו ולחפש בו בזמן ריצה. const enum מוסר בזמן הקומפילציה וכל שימוש בו מוחלף בערך שלו (Size.Large הופך ל-2), כך שהוא לא עולה כלום בזמן ריצה, אבל אי אפשר לעבור עליו, וכלים שמקמפלים קובץ אחד בכל פעם מגבילים אותו.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל