Menu

TypeScript readonly: מאפיינים, Readonly<T> ומערכים

המגדיר readonly וה-utility type Readonly<T> מונעים מקוד לבצע השמה מחדש למאפיינים. למדו מאפייני readonly ושדות מחלקה, Readonly<T>, מערכים לקריאה בלבד (readonly T[] ו-ReadonlyArray), ReadonlyMap ו-ReadonlySet, למה readonly רדוד וקיים רק בזמן קומפילציה, ואיך הוא משתווה ל-Object.freeze ול-as const.

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

readonly מסמן מאפיין שאפשר לקבוע פעם אחת, כשהאובייקט נוצר, ואף פעם לא לבצע לו השמה מחדש. Readonly<T> מחיל אותו על כל מאפיין של טיפוס, ו-readonly T[] עושה אותו דבר למערכים:

השורה האחרונה מראה את העובדה החשובה ביותר על readonly: הקומפיילר בודק אותו, אבל הוא לא נאכף בזמן ריצה. ההשמה הייתה שגיאת קומפילציה (שהושתקה כאן עם @ts-expect-error), ובכל זאת ה-JavaScript שנוצר הריץ אותה. בלי ההשתקה, הקובץ לא היה עובר קומפילציה, ושם readonly עושה את העבודה שלו.

מאפייני readonly

כתבו readonly לפני שם מאפיין ב-interface, בליטרל טיפוס או במחלקה. אפשר לאתחל את המאפיין, אבל אי אפשר לבצע לו השמה מחדש:

במחלקה, אפשר לבצע השמה לשדה readonly בהצהרה שלו או בבנאי, ובשום מקום אחר. הצורה הקצרה ביותר היא parameter property, constructor(readonly id: string) {}, שמצהירה על השדה ומבצעת לו השמה בצעד אחד. דף המחלקות מכסה שדות ובנאים באופן כללי.

Readonly<T>: כל המאפיינים בבת אחת

Readonly<T> הוא utility type שמסמן כל מאפיין של T כ-readonly. הוא שימושי לערכים שמעבירים ממקום למקום אבל אסור לשנות, כמו state של אפליקציה:

חתימת הפונקציה אומרת לקורא ש-addItem מחזירה state חדש במקום לשנות את הישן, והקומפיילר מחזיק את הפונקציה להבטחה הזו. Readonly<T> מוגדר כ-mapped type { readonly [P in keyof T]: T[P] }.

מערכים לקריאה בלבד: readonly T[] ו-ReadonlyArray<T>

readonly number[] ו-ReadonlyArray<number> הם אותו טיפוס. הם מסירים כל מתודה משנה (push, pop, shift, splice, sort, reverse, fill...) ואוסרים השמה לפי אינדקס. המתודות שלא משנות נשארות ומחזירות מערכים רגילים:

קבלת readonly T[] כפרמטר היא הבטחה למי שקורא לפונקציה שלא תשנו את המערך שלו. הכיוון השני הוא המקום שבו אנשים נתקעים: אי אפשר להעביר מערך לקריאה בלבד לפונקציה שמקבלת T[] רגיל, כי הפונקציה הזו עלולה לשנות אותו.

index.ts(7,17): error TS4104: The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.

הפתרון הוא לשנות את sum כך שתקבל readonly number[], כי היא לא משנה כלום. פונקציות שרק קוראות מערך צריכות תמיד לקבל את הטיפוס לקריאה בלבד; כך הן מקבלות את שני הסוגים. אם הפונקציה לא שלכם, העבירו עותק: sum([...prices]).

ReadonlyMap ו-ReadonlySet

גם ל-Maps ול-sets יש גרסאות לקריאה בלבד. ל-ReadonlyMap<K, V> יש get, has, size, forEach והאיטרטורים, אבל אין set, delete או clear; ל-ReadonlySet<T> אין add, delete או clear:

מחלקה לרוב שומרת Map פרטי שניתן לשינוי וחושפת אותו דרך getter מטיפוס ReadonlyMap, כך שקוד חיצוני יכול לקרוא את הנתונים אבל לא לשנות אותם דרך ההפניה הזו.

readonly הוא רדוד

readonly ו-Readonly<T> מגנים רק על המאפיין עצמו, ולא על האובייקט או המערך שהוא מצביע עליו:

DeepReadonly<T> מפעיל את עצמו על כל טיפוס אובייקט מקונן, ומכיוון ש-mapped type על טיפוס מערך מייצר מערך לקריאה בלבד, members הופך ל-readonly string[]. זו עדיין הבטחה ברמת הטיפוסים, ולא הגנה בזמן ריצה.

רק בזמן קומפילציה: שינוי דרך הפניה אחרת

טיפוס לקריאה בלבד שולט במה שהפניה אחת רשאית לעשות. הפניה אחרת לאותו אובייקט, בלי readonly בטיפוס שלה, יכולה לשנות אותו, ו-TypeScript אפילו מתיר לשייך טיפוס לקריאה בלבד לטיפוס שניתן לשינוי:

ההשמה mutable = settings עוברת קומפילציה כי TypeScript לא מתחשב במאפייני readonly כשהוא בודק אם שני טיפוסי אובייקט תואמים; ה-handbook של TypeScript אומר זאת במפורש, ומציין שלכן מאפייני readonly יכולים להשתנות דרך aliasing. מערכים לקריאה בלבד שונים: שגיאת TS4104 שלמעלה היא בדיוק הבדיקה הזו. Object.freeze באמת מונע שינויים בזמן ריצה: הקוד שנוצר רץ ב-strict mode, שבו כתיבה למאפיין מוקפא זורקת TypeError. כמו readonly, גם Object.freeze רדוד.

readonly, const, as const ו-Object.freeze

as const על ליטרל הופך כל מאפיין ל-readonly בכל עומק ושומר על טיפוסים ליטרליים, ולרוב זו הדרך הקלה ביותר לקבל ערך שהוא לקריאה בלבד לעומק:

const theme = { mode: "dark", sizes: [12, 14] } as const;
// { readonly mode: "dark"; readonly sizes: readonly [12, 14] }
חל עלעמוק?השפעה בזמן ריצהדוגמה
constקשירת משתנהלאאי אפשר לבצע השמה מחדש למשתנהconst user = {...}
readonlyמאפיין אחד או טיפוס מערךלאאיןreadonly id: string
Readonly<T>כל מאפיין של טיפוסלאאיןReadonly<State>
as constביטוי ליטרליכןאין{ ... } as const
Object.freezeערך אובייקטלאכתיבות נכשלות (זורקות ב-strict mode)Object.freeze(obj)

const ו-readonly עונים על שאלות שונות: const מונע מהשם להצביע למקום אחר, readonly מונע ממאפיין להשתנות. למאפיינים של אובייקט const עדיין אפשר לבצע השמה מחדש, אלא אם הם readonly.

שאלות נפוצות

מה readonly עושה ב-TypeScript?

readonly מסמן מאפיין שאפשר לקבוע כשהאובייקט נוצר (או בבנאי של מחלקה), אבל אי אפשר לבצע לו השמה מחדש אחר כך. השמה אליו מאוחר יותר היא שגיאת קומפילציה, TS2540. זו רק בדיקת טיפוסים: ב-JavaScript שנוצר אין שום הגנה.

מה ההבדל בין readonly ל-const ב-TypeScript?

const נוגע למשתנה: אי אפשר לכוון את השם לערך אחר, אבל את האובייקט שהוא מחזיק עדיין אפשר לשנות. readonly נוגע למאפיין: אי אפשר לבצע למאפיין הזה השמה מחדש. const user = { name: "Ada" } עדיין מתיר user.name = "x"; מאפיין readonly name לא מתיר.

איך הופכים מערך לקריאה בלבד ב-TypeScript?

תנו לו את הטיפוס readonly T[] או ReadonlyArray<T> (אותו טיפוס). מתודות משנות כמו push, pop, sort ו-splice נעלמות מהטיפוס, והשמה לפי אינדקס היא שגיאה. מתודות שלא משנות כמו map, filter ו-slice עדיין עובדות ומחזירות מערכים רגילים.

האם Readonly עמוק ב-TypeScript?

לא. Readonly<T> ו-readonly מגנים רק על המאפיינים ברמה העליונה; אובייקטים ומערכים מקוננים בפנים עדיין ניתנים לשינוי. להגנה עמוקה ברמת הטיפוסים, השתמשו ב-as const על ליטרל, או כתבו טיפוס רקורסיבי DeepReadonly<T>.

האם readonly מונע שינויים בזמן ריצה?

לא. טיפוסים נמחקים, ולכן מאפיין readonly הוא מאפיין רגיל בזמן ריצה, וקוד עם הפניה ניתנת לשינוי לאותו אובייקט (או JavaScript רגיל) עדיין יכול לשנות אותו. השתמשו ב-Object.freeze כשצריך הגנה בזמן ריצה; TypeScript נותן לתוצאה שלו את הטיפוס Readonly<T>.

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

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

להתחיל