Menu

Dictionary ב-TypeScript: Index Signatures, Record ו-Map

ב-TypeScript אין מחלקת dictionary או hashmap; נותנים טיפוס לחיפוש מפתח-ערך עם index signature, עם Record<K, V> או עם Map<K, V>. כאן תלמדו כל אחד מהם, איך בודקים אם מפתח קיים, איך מוסיפים, מוחקים ועוברים על רשומות, ולמה noUncheckedIndexedAccess חשוב למילונים.

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

ב-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) !== undefinedMapאותה הסתייגות של 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 לא כולל את האפשרות הזו.

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

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

להתחיל