Record<K, V> הוא utility type מובנה לאובייקט שהמפתחות שלו מטיפוס K וכל הערכים שלו מטיפוס V. עם מפתחות string הוא מתאר מילון; עם איחוד של מפתחות ליטרליים הוא מתאר אובייקט שחייב להכיל בדיוק את המפתחות האלה.
Record קיים רק במערכת הטיפוסים. בזמן ריצה שני האובייקטים הם אובייקטי JavaScript רגילים, ולכן הם עובדים עם ליטרלי אובייקט, spread, JSON.stringify וכל דבר אחר שמקבל אובייקט.
תחביר והגדרה
Record<Keys, Value>
Keys חייב להיות משהו שיכול לשמש מפתח של אובייקט: string, number, symbol, איחוד של ליטרלים מסוג מחרוזת או מספר, או template literal type. Value יכול להיות כל טיפוס. ההגדרה כולה בספרייה הסטנדרטית של TypeScript היא שורה אחת, mapped type:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
keyof any הוא string | number | symbol, קבוצת כל טיפוסי המפתח האפשריים. [P in K]: T יוצר מאפיין אחד מטיפוס T לכל איבר של K. זה מסביר את שתי ההתנהגויות שבהמשך: K רחב כמו string מייצר index signature (כל מפתח), ו-K שהוא איחוד מייצר מאפיין חובה אחד לכל איבר.
מפתחות איחוד: כל מפתח הוא חובה
כשהמפתחות הם איחוד של ליטרלים, Record חייב לפרט כל אחד מהם, ולא אף מפתח אחר. זה הופך את הקומפיילר לרשימת תיוג:
הוסיפו "cancelled" ל-Status, ושני האובייקטים יפסיקו לעבור קומפילציה עד שתתנו לסטטוס החדש תווית וצבע. השמטת מפתח, או הוספת מפתח שלא נמצא באיחוד, היא שגיאת קומפילציה:
index.ts(4,7): error TS2741: Property 'error' is missing in type '{ idle: string; loading: string; success: string; }' but required in type 'Record<Status, string>'.
index.ts(11,54): error TS2353: Object literal may only specify known properties, and 'paused' does not exist in type 'Record<Status, string>'.
אותו דבר עובד עם string enum כטיפוס המפתח: Record<Color, string> דורש רשומה אחת לכל איבר של ה-enum.
Record<string, T> והמפתח החסר
עם מפתחות string, כל מפתח מותר, ו-TypeScript נותן לכל שליפה את הטיפוס T, גם למפתח שלא קיים. בזמן ריצה מפתח חסר נותן undefined:
זה הבאג הנפוץ ביותר עם Record. שלוש דרכים לטפל בו: לבדוק עם in או Object.hasOwn לפני הקריאה, להצהיר על הערך כ-V | undefined, או להפעיל את אפשרות הקומפיילר noUncheckedIndexedAccess, שמוסיפה | undefined לכל קריאה דרך index signature בפרויקט. ל-Record עם מפתחות איחוד אין את הבעיה הזו, כי מובטח שכל מפתח קיים.
Partial<Record<K, V>>: רק חלק מהמפתחות
כדי להשתמש באיחוד של מפתחות בלי לדרוש את כולם, עטפו את ה-Record ב-Partial. אז קריאות מחזירות V | undefined, וזה כן:
מפתח עם שגיאת כתיב כמו jp הוא עדיין שגיאה, וזה היתרון לעומת Record<string, string>.
איטרציה על Record
Object.keys, Object.values ו-Object.entries כולם עובדים. הבעיה היא טיפוס המפתח: Object.keys מחזיר string[] ו-Object.entries מחזיר [string, V][], ואף פעם לא את איחוד המפתחות שלכם:
TypeScript משאיר את המפתחות כ-string בכוונה: לאובייקט יכולים להיות בזמן ריצה יותר מאפיינים ממה שהטיפוס שלו מפרט, ולכן הבטחה של Plan[] הייתה לא בטוחה באופן כללי. לאובייקט שיצרתם מליטרל, כמו seats, ה-cast בטוח.
בניית Record מנתונים
Records הם התוצאה הרגילה של קיבוץ או של יצירת אינדקס ממערך. התחילו מאובייקט ריק מטיפוס Record ומלאו אותו:
Book["genre"] משתמש מחדש באיחוד מה-interface כטיפוס המפתח, ולכן הוספת ז'אנר ל-Book גורמת ל-byGenre לדרוש רשומה חדשה.
Record<string, unknown> ו-interfaces
Record<string, unknown> הוא טיפוס נפוץ ל"אובייקט כלשהו עם מפתחות מחרוזת". הוא מקבל ליטרלי אובייקט וערכים שהטיפוס שלהם הוגדר עם type alias, אבל interface נדחה:
index.ts(11,11): error TS2345: Argument of type 'User' is not assignable to parameter of type 'Record<string, unknown>'.
Index signature for type 'string' is missing in type 'User'.
אפשר להרחיב interfaces באמצעות מיזוג הצהרות, ולכן TypeScript לא מניח שהם מתאימים ל-index signature; type aliases אי אפשר לפתוח מחדש, ולכן type User = { name: string } היה עובר. הפתרונות הרגילים הם לקבל במקום זאת object (עדיין אפשר לקרוא עליו ל-Object.keys), להפוך את הפונקציה לגנרית (<T extends object>(obj: T)), או להצהיר על User עם type (דף interface מול type מכסה את שאר ההבדלים).
Record מול index signature מול Map
Record<K, V> | { [key: string]: V } | Map<K, V> | |
|---|---|---|---|
| קיים בזמן ריצה | לא, אובייקט רגיל | לא, אובייקט רגיל | כן, מחלקה |
| קבוצה קבועה של מפתחות | כן, עם K שהוא איחוד | לא | לא |
| טיפוסי מפתח | string, number, symbol, איחודי ליטרלים, תבניות template | string, number, symbol, תבניות template | כל דבר, כולל אובייקטים |
| טיפוס שליפה של מפתח חסר | V (עם מפתחות string) | V | V | undefined מ-get |
| שילוב עם מאפיינים בעלי שם | דרך חיתוך & | כן, באותו טיפוס | לא |
| JSON ו-spread | כן | כן | לא, צריך להמיר קודם |
| גודל | Object.keys(r).length | Object.keys(o).length | m.size |
| הוספה ומחיקה תכופות | עובד | עובד | בנוי לזה |
בחרו ב-Record עם מפתחות איחוד בכל פעם שקבוצת המפתחות ידועה: זו האפשרות היחידה שבודקת שכל מפתח קיים. למפתחות מחרוזת פתוחים, Record<string, V> ו-index signature ניתנים להחלפה, ובסיסי קוד רבים מעדיפים את Record בגלל הקריאות. פנו ל-Map כשמפתחות נוספים ומוסרים בזמן ריצה, כשהמפתחות אינם מחרוזות, או כשצריך את הגודל ואת סדר ההכנסה בלי עבודה נוספת.
שאלות נפוצות
מה זה Record ב-TypeScript?
Record<K, V> הוא utility type מובנה לאובייקט שהמפתחות שלו מטיפוס K וכל הערכים שלו מטיפוס V. Record<string, number> הוא אובייקט עם כל מפתח מחרוזת וערכים מספריים; Record<"en" | "de", string> הוא אובייקט עם בדיוק המפתחות en ו-de, ושניהם מחרוזות.
מה ההבדל בין Record ל-Map ב-TypeScript?
Record הוא טיפוס לאובייקט JavaScript רגיל, ולכן הוא עובד עם ליטרלי אובייקט, JSON ו-spread, והוא נעלם בזמן קומפילציה. Map היא מחלקה שקיימת בזמן ריצה עם get, set, has ו-size, שומרת על סדר ההכנסה לכל המפתחות, מקבלת כל טיפוס מפתח (גם אובייקטים), ו-get מחזיר V | undefined. השתמשו ב-Record לנתונים קבועים או בצורת JSON, וב-Map למפתחות שנוספים ומוסרים בזמן ריצה.
מה ההבדל בין Record<string, T> לבין { [key: string]: T }?
מבחינת ערכים הם אותו טיפוס: Record<string, T> מתפרש לטיפוס אובייקט עם index signature של מחרוזת. שני הבדלים קטנים: ל-index signature יכול להיות שם, והוא יכול לשבת ליד מאפיינים אחרים באותו טיפוס, ו-keyof Record<string, T> הוא string בזמן ש-keyof { [key: string]: T } הוא string | number.
איך עוברים בלולאה על Record ב-TypeScript?
השתמשו ב-Object.entries(record) לזוגות מפתח וערך, ב-Object.keys למפתחות וב-Object.values לערכים. המפתחות חוזרים כ-string ולא כ-K, כי לאובייקט יכולים להיות מפתחות נוספים בזמן ריצה. כשל-Record יש איחוד של מפתחות ידועים, בצעו cast: (Object.keys(r) as Array<keyof typeof r>).
איך הופכים רק חלק מהמפתחות של Record לחובה?
עם מפתחות איחוד, Record<K, V> דורש כל מפתח. עטפו אותו ב-Partial כדי להפוך את כולם לאופציונליים: Partial<Record<Lang, string>>. לשילוב, השתמשו בחיתוך: Record<"en", string> & Partial<Record<"de" | "fr", string>> דורש את en ומתיר את האחרים.