ב-TypeScript אין מחלקה נפרדת של dictionary או hashmap. מילון הוא או אובייקט רגיל עם טיפוס של index signature, { [key: string]: number }, אותו טיפוס שנכתב כ-Record<string, number>, או Map<string, number>. שלושתם שומרים ערכים לפי מפתח; ההבדלים הם בטיפוסי המפתחות, באופן שבו מפתחות חסרים מקבלים טיפוס ובאופן שבו הם עוברים סריאליזציה.
למפתחות שהם מחרוזות ולמידע במבנה של JSON, אובייקט עם Record<string, T> הוא הבחירה הרגילה. למפתחות שאינם מחרוזות, או לרשומות שמתווספות ומוסרות כל הזמן, השתמשו ב-Map.
Index Signatures
index signature, [key: KeyType]: ValueType, אומר "כל מפתח מהטיפוס הזה ממופה לערך מהטיפוס הזה". שם המפתח (key, name, userId) הוא רק תיעוד. טיפוסי מפתח יכולים להיות string, number, symbol, תבניות template literal, או unions שלהם.
JavaScript ממירה מפתחות מספריים למחרוזות, ולכן לאובייקט { [id: number]: string } עדיין יש מפתחות מחרוזת בזמן ריצה: Object.keys({ 1: "one" }) הוא [ '1' ]. ה-index signature המספרי רק מגביל איך מותר לגשת אליו ב-TypeScript.
Record<K, V>
Record<string, V> הוא קיצור של { [key: string]: V }. עם union של מפתחות ליטרליים במקום string, הוא הופך למילון קבוע שחייב להכיל כל מפתח:
השמטה של staging מ-urls היא שגיאת הקומפילציה TS2741 (Property 'staging' is missing...), וזה הופך את Record עם מפתחות union לטבלת חיפוש שנבדקת. Partial הופך כל ערך ל-number | undefined.
Map כ-Hash Map
Map מקבל מפתחות מכל טיפוס, שומר על סדר ההכנסה, יש לו size, והוא נותן טיפוס כן למפתחות חסרים: get מחזיר V | undefined.
בדיקה אם מפתח קיים
יש כמה בדיקות, והן לא כולן אומרות את אותו הדבר:
| בדיקה | עובדת על | שימו לב |
|---|---|---|
Object.hasOwn(obj, key) | אובייקטים | ES2022; ב-targets ישנים השתמשו ב-Object.prototype.hasOwnProperty.call(obj, key) |
key in obj | אובייקטים | נכון גם למפתחות שנורשו כמו toString ו-constructor |
obj[key] !== undefined | אובייקטים | לא מבחינה בין מפתח חסר למפתח שנשמר בו undefined |
if (obj[key]) | אובייקטים | שקרית גם לערכים 0, "" ו-false |
map.has(key) | Map | לא מצמצמת map.get(key) שבא אחריה |
map.get(key) !== undefined | Map | אותה הסתייגות של undefined כמו באובייקטים |
הבעיה של מפתחות שנורשו היא הסיבה שמפתחות שמגיעים ממשתמשים, כמו "constructor" או "__proto__", הופכים אובייקטים רגילים למילונים מסוכנים. ל-Map אין מפתחות כאלה.
בעיית הטיפוס של מפתח חסר
ב-index signature או ב-Record<string, T>, לקריאה של כל מפתח יש את הטיפוס T, גם למפתח שלא קיים. הקומפיילר מאפשר לקרוא למתודות על ערך שהוא undefined בזמן ריצה:
אפשרות הקומפיילר noUncheckedIndexedAccess מתקנת את זה: איתה, ל-colors["grass"] יש את הטיפוס string | undefined, והקריאה ל-toUpperCase היא שגיאת קומפילציה עד שבודקים אותו. היא לא חלק מ-strict, ולכן צריך להפעיל אותה בנפרד ב-tsconfig.json; ראו strict mode לשאר הדגלים שלצידה. ל-Map אין פער כזה, כי get תמיד כולל undefined.
הוספה, הסרה ומעבר על רשומות
delete עובד על מאפיינים של index signature. על מאפיין חובה בעל שם בטיפוס אובייקט, הוא שגיאת הקומפילציה TS2790, The operand of a 'delete' operator must be optional.
במה להשתמש
| צורך | השתמשו ב |
|---|---|
| מפתחות מחרוזת, JSON שנכנס או יוצא | Record<string, T> |
| קבוצת מפתחות קבועה וידועה, כולם חובה | Record<"a" | "b", T> |
| מאפיינים בעלי שם ובנוסף מפתחות שרירותיים | טיפוס אובייקט עם index signature |
| מפתחות שהם אובייקטים, מספרים שנשארים מספרים, או כל דבר שאינו מחרוזת | Map<K, V> |
| הרבה הוספות ומחיקות, או גודל שקוראים לעתים קרובות | Map<K, V> |
| מפתחות שמגיעים ממשתמשים | Map<K, V> (בלי מפתחות שנורשו) |
שאלות נפוצות
איך יוצרים dictionary ב-TypeScript?
תנו לאובייקט רגיל טיפוס עם index signature, const ages: { [name: string]: number } = {}, או את המקבילה Record<string, number>. אחר כך הוסיפו רשומות עם ages["ada"] = 36. למפתחות שאינם מחרוזות, או לאוסף עם הרבה הוספות ומחיקות, השתמשו ב-new Map<string, number>().
האם יש ב-TypeScript HashMap?
לא בשם הזה. ה-Map המובנה של JavaScript הוא hash map: Map<K, V> שומר זוגות של מפתח וערך עם חיפוש מהיר לפי מפתח, שומר על סדר ההכנסה ומקבל כל טיפוס מפתח. אובייקט רגיל מטיפוס Record<string, V> הוא הבחירה הנפוצה השנייה למפתחות שהם מחרוזות.
איך בודקים אם מפתח קיים ב-dictionary ב-TypeScript?
במילון שהוא אובייקט, השתמשו ב-Object.hasOwn(dict, key) או ב-key in dict (שרואה גם מאפיינים שנורשו כמו toString), או קראו את הערך והשוו אותו ל-undefined. ב-Map, השתמשו ב-map.has(key), או בדקו ישירות את התוצאה של map.get(key), כי has לא מצמצם get שבא אחריו.
מה ההבדל בין index signature ל-Record?
{ [key: string]: T } ו-Record<string, T> מתארים את אותו טיפוס. Record קצר יותר ויכול לקבל גם union של מפתחות מסוימים, Record<"a" | "b", T>, שדורש כל מפתח. index signature אפשר לשלב עם מאפיינים בעלי שם באותו טיפוס אובייקט, ולמפתח שלו אפשר לתת שם שמתעד אותו, כמו ב-{ [userId: string]: User }.
למה קריאה של מפתח חסר במילון לא נותנת שגיאה?
כברירת מחדל, ל-dict[key] על index signature או על Record<string, T> יש את הטיפוס T, למרות שבזמן ריצה הערך הוא undefined למפתח חסר. הפעילו את noUncheckedIndexedAccess ב-tsconfig.json והטיפוס יהפוך ל-T | undefined, מה שמחייב בדיקה. strict לא כולל את האפשרות הזו.