literal type הוא טיפוס עם ערך אחד בדיוק: "up" הוא טיפוס שהאיבר היחיד שלו הוא המחרוזת "up", ו-404 הוא טיפוס שהאיבר היחיד שלו הוא המספר 404. לבדם הם לא שימושיים במיוחד. כשמחברים אותם ל-union, מקבלים משתנה שמקבל קבוצה קבועה של ערכים ותו לא.
בלי ההערה @ts-expect-error, הקריאה האחרונה היא שגיאה בזמן קומפילציה (TS2345). איתה, התוכנית מתקמפלת, והקריאה עדיין רצה ומדפיסה moving north by 1: literal types קיימים רק בשביל הקומפיילר, ובזמן ריצה הערך הוא מחרוזת רגילה.
Literals של String, Number ו-Boolean
כל ערך של string, number, bigint או boolean אפשר לכתוב כטיפוס. הקומפיילר יקבל אז רק את הערך המדויק הזה.
let method: "GET" = "GET";
let code: 404 = 404;
let ok: true = true;
let big: 10n = 10n;
type Port = 80 | 443 | 8080;
const port: Port = 443;
boolean עצמו הוא פשוט ה-union true | false, ולכן צמצום של boolean עם if (flag) משאיר את false בענף ה-else.
| Literal type | מאפשר | טיפוס רחב יותר |
|---|---|---|
"GET" | רק את המחרוזת "GET" | string |
404 | רק את המספר 404 | number |
10n | רק את ה-bigint 10 | bigint |
true | רק את true | boolean |
Unions של Literals
השימוש הנפוץ הוא union שמפרט כל ערך מותר. בתוך הפונקציה הקומפיילר מצמצם את ה-union בזמן שאתם בודקים אותו, כך שכל ענף יודע בדיוק איזה ערך יש לו.
union של string literals הוא החלופה המקובלת ל-enum ב-TypeScript. הוא לא עולה כלום בזמן ריצה, הערכים הם מחרוזות רגילות שאפשר להדפיס ללוג ולשלוח ב-JSON, ושגיאת הקלדה היא שגיאת קומפילציה. עוד על היתרונות והחסרונות ב-enums.
Widening: let מול const
כש-TypeScript מסיק טיפוס מ-literal, הוא בודק אם הערך יכול להשתנות. אי אפשר אף פעם להשים ערך חדש למשתנה const, ולכן הוא שומר על ה-literal type. למשתנה let אפשר, ולכן הטיפוס שלו מורחב לטיפוס הכללי.
רחפו עם העכבר מעל כל שם בעורך כדי לראות את הטיפוס שהוסק. אם אתם רוצים let שמכיל רק ערכים מסוימים, תנו לו annotation: let mode: "light" | "dark" = "light".
למה מאפייני אובייקט מתרחבים
מאפיינים של object literal ניתנים לשינוי, ולכן גם הם מתרחבים, אפילו כשהאובייקט שמור ב-const. זו הדרך הנפוצה ביותר להיתקל ב-literal types בלי כוונה:
הקומפיילר מדווח:
index.ts(7,15): error TS2345: Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.
הטיפוס שמוסק ל-req הוא { url: string; method: string }, כי קוד בהמשך יכול להריץ req.method = "DELETE". יש שלושה תיקונים:
אפשרות רביעית היא satisfies, שבודק את האובייקט מול טיפוס ובאותו זמן שומר על ה-literal types של המאפיינים שלו.
as const
as const הוא const assertion. שימו אותו אחרי ביטוי, והקומפיילר יסיק את הטיפוס הצר ביותר שהוא יכול:
- ערכי string, number ו-boolean שומרים על ה-literal types שלהם
- מאפייני אובייקט הופכים ל-
readonly - array literals הופכים ל-tuples של
readonlyבאורך קבוע
ה-assertion קיים רק בזמן קומפילציה. ה-JavaScript שנוצר הוא אותו object literal בלי as const, ולכן שום דבר לא מונע מקוד אחר לשנות אותו בזמן ריצה. אם אתם צריכים הבטחה בזמן ריצה, קראו גם ל-Object.freeze.
טיפוס Union ממערך as const
דפוס נפוץ הוא לשמור את הערכים המותרים במערך אחד, שאפשר לעבור עליו בלולאה בזמן ריצה, ולגזור ממנו את טיפוס ה-union. (typeof arr)[number] פירושו "הטיפוס של כל איבר ב-arr".
בלי as const, ROLES היה string[] ו-Role היה string רגיל. ה-cast ל-readonly string[] ב-isRole נחוץ כי includes על tuple של literals מקבל רק את ה-literals האלה, והמטרה של הפונקציה היא לבדוק מחרוזת שאולי אינה אחד מהם. אותו דפוס עם אובייקט עובד גם למפות של מפתח וערך: const Status = { Active: "active", Banned: "banned" } as const, ואז type Status = (typeof Status)[keyof typeof Status].
const Type Parameters
פונקציה גנרית בדרך כלל מרחיבה את ה-literals שמעבירים לה. מאז TypeScript 5.0 אפשר לסמן type parameter כ-const, וכך הקומפיילר מסיק את הארגומנט כאילו היה עליו as const, בלי לבקש מהקורא לכתוב את זה.
זה בעיקר כלי לכותבי ספריות: הגדרות routes, builders ו-helpers של סכמות משתמשים בו כדי שהקוראים יקבלו טיפוסים מדויקים מ-literals רגילים.
המשמעויות של const
מילת המפתח const מופיעה בארבעה מקומות שונים בקוד TypeScript:
| תחביר | סוג | מה הוא עושה |
|---|---|---|
const x = 1 | הצהרה של JavaScript | אי אפשר להשים ערך חדש למשתנה; ערך literal שומר על ה-literal type שלו |
expr as const | assertion של TypeScript | הטיפוס הצר ביותר: literals, מאפייני readonly, tuples של readonly |
function f<const T>() | type parameter של TypeScript | מסיק ארגומנטים כאילו היה עליהם as const |
const enum E {} | enum של TypeScript | enum שהאיברים שלו מוטמעים בקוד בזמן קומפילציה |
אף אחד מהם לא מקפיא אובייקט בזמן ריצה. const obj = { a: 1 } עדיין מאפשר obj.a = 2; רק השמה מחדש של obj עצמו היא שגיאה.
שאלות נפוצות
מה זה literal type ב-TypeScript?
טיפוס שמאפשר ערך אחד בדיוק. "GET" הוא טיפוס שהערך היחיד שלו הוא המחרוזת "GET", 404 הוא טיפוס שהערך היחיד שלו הוא המספר 404, ו-true הוא טיפוס שהערך היחיד שלו הוא true. הם הכי שימושיים כשמשלבים אותם ל-unions, כמו type Method = "GET" | "POST".
מה as const עושה ב-TypeScript?
as const הוא const assertion. הוא אומר לקומפיילר להסיק את הטיפוס הצר ביותר לביטוי: ערכי מחרוזת ומספר שומרים על ה-literal types שלהם, מאפייני אובייקט הופכים ל-readonly, ו-array literals הופכים ל-tuples של readonly. הוא משנה רק את הטיפוס; הערך בזמן ריצה הוא אותו אובייקט או מערך רגיל, והוא לא מוקפא.
למה TypeScript מסיק string במקום ה-literal שלי?
כי הערך ניתן לשינוי. אפשר להשים ערך חדש ל-let x = "a" ולמאפיין ב-{ method: "GET" } בהמשך, ולכן TypeScript מרחיב אותם ל-string. משתנה const שומר על ה-literal type "a". כדי לשמור על literals בתוך אובייקט, תנו לו annotation עם ה-literal type, השתמשו ב-as const, או השתמשו ב-satisfies.
מה ההבדל בין const ל-as const?
const הוא הצהרה של JavaScript: אי אפשר להשים ערך חדש למשתנה, אבל עדיין אפשר לשנות את האובייקט שהוא מצביע עליו. as const הוא type assertion של TypeScript: הוא הופך כל מאפיין של הערך ל-readonly ול-literal במערכת הטיפוסים. אף אחד מהם לא מקפיא את האובייקט בזמן ריצה; לשם כך השתמשו ב-Object.freeze.
איך מקבלים טיפוס union ממערך של מחרוזות?
הצהירו על המערך עם as const, ואז גשו לטיפוס שלו עם number: const roles = ["admin", "user"] as const; type Role = (typeof roles)[number]; נותן "admin" | "user". בלי as const המערך הוא string[] והתוצאה היא פשוט string.