Menu

keyof ב-TypeScript: המפתחות של אובייקט כטיפוס, עם דוגמאות

keyof מקבל טיפוס אובייקט ומחזיר את ה-union של שמות המאפיינים שלו. כאן תלמדו keyof עם interfaces, keyof typeof לאובייקטים רגילים, גישה מוטפסת למאפיינים עם גנריקס, index signatures (string | number), ולמה Object.keys מחזיר string[].

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

keyof מקבל טיפוס אובייקט ומייצר את ה-union של שמות המאפיינים שלו. keyof User שלמטה הוא הטיפוס "id" | "name" | "email", ולכן משתנה מהטיפוס הזה יכול להכיל רק אחת משלוש המחרוזות האלה.

keyof קיים רק במערכת הטיפוסים. הוא נמחק כשהקוד מתקמפל, ולכן הוא אף פעם לא בודק שום דבר בזמן ריצה. מה שהוא נותן לכם הוא הבטחה בזמן קומפילציה שמפתח הוא שם של מאפיין אמיתי: שנו את השם של email ב-interface, וכל "email" שהטיפוס שלו keyof User יהפוך לשגיאה.

keyof typeof: המפתחות של אובייקט רגיל

keyof עובד על טיפוסים, לא על ערכים. עבור אובייקט שכתבתם כערך, קבלו קודם את הטיפוס שלו עם typeof, ואז קחו את המפתחות:

קראו את זה מבפנים החוצה: typeof colors הוא { red: string; green: string; blue: string }, ו-keyof שלו הוא ה-union של המפתחות. כתיבת keyof colors בלי typeof נכשלת עם error TS2749: 'colors' refers to a value, but is being used as a type here. Did you mean 'typeof colors'?.

זו הדרך המקובלת לגזור union מטבלת חיפוש: הוסיפו צבע לאובייקט, והטיפוס Color יגדל יחד איתו. ל-typeof יש גם משמעות שנייה, בזמן ריצה; העמוד על typeof מסביר את שתיהן.

גישה מוטפסת למאפיינים עם גנריקס

השימוש הנפוץ ביותר ב-keyof הוא אילוץ גנרי. K extends keyof T אומר "K הוא אחד המפתחות של T", ו-T[K] הוא הטיפוס של המאפיין במפתח הזה:

פרמטר הטיפוס השני חשוב. עם key: keyof T ובלי K, טיפוס ההחזרה הוא T[keyof T], ה-union של כל טיפוסי המאפיינים (כאן string | number | boolean), ולכן title.toUpperCase() היה נכשל עם error TS2339: Property 'toUpperCase' does not exist on type 'string | number | boolean'. לכידת המפתח המדויק ב-K שומרת על הטיפוס המדויק של המאפיין. T[K] הוא indexed access type, שמוסבר בעמוד על indexed access types.

גישה עם string רגיל

שגיאה נפוצה מאוד מופיעה כשניגשים לאובייקט עם string שהגיע ממקום אחר:

הקומפיילר מדפיס:

index.ts(10,10): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Settings'.
  No index signature with a parameter of type 'string' was found on type 'Settings'.

כל מחרוזת יכולה להגיע ל-read, כולל "colour", ו-Settings לא אומר כלום על המפתח הזה. יש שני תיקונים: לקבל רק מפתחות אמיתיים, או לבדוק מחרוזת לא מוכרת לפני שמשתמשים בה.

name is keyof Settings הוא type predicate: כשהפונקציה מחזירה true, TypeScript מתייחס ל-input כמפתח בתוך ה-if. העמוד על type guards מסביר predicates בפירוט.

keyof עם Index Signatures: string | number

עבור טיפוס עם index signature, keyof מחזיר את טיפוס האינדקס, ו-string index signature נותן string | number, לא רק string:

JavaScript ממיר כל מפתח מספרי של מאפיין למחרוזת, ולכן scores[42] ו-scores["42"] הם אותו מאפיין. TypeScript מבטא את זה בכך שהוא כולל את number. הפלט מראה גם כלל של JavaScript: מפתחות שנראים כמו מספרים שלמים מופיעים ראשונים, בסדר עולה, לפני שאר מפתחות המחרוזת. כשפונקציה גנרית צריכה רק מפתחות מחרוזת, כתבו Extract<keyof T, string> או string & keyof T; ל-Exclude ו-Extract יש עמוד משלהם.

תוצאות keyof במבט אחד

טיפוסkeyof נותן
{ id: number; name: string }"id" | "name"
{ [key: string]: number }string | number
{ [index: number]: string }number
{}never
anystring | number | symbol
{ a: 1; b: 2 } | { a: 3; c: 4 }"a" (המפתחות שיש לכל האיברים)
{ a: 1 } & { c: 4 }"a" | "c" (המפתחות של כל אחד מהם)
string[]number ועוד השם של כל מתודת מערך ("length", "push", ...)

השורות של union ושל intersection נראות הפוכות בהתחלה. ערך מטיפוס union יכול להיות כל אחד מהאיברים, ולכן רק מפתחות שקיימים בשניהם בטוחים. לערך מטיפוס intersection יש הכול משניהם, ולכן יש לו את כל המפתחות.

למה Object.keys מחזיר string[]

ל-Object.keys(obj) יש הטיפוס string[], אף פעם לא (keyof T)[]. הסיבה היא structural typing: ערך יכול לשאת יותר מאפיינים מהטיפוס שהוצהר עליו.

הטיפוס של p הוא Point, ובכל זאת יש לו מפתח z בזמן ריצה. אילו Object.keys היה מבטיח ("x" | "y")[], קוד שמסתמך על זה היה שגוי עבור p. ה-cast בטוח עבור object literals שיצרתם בעצמכם; עבור אובייקטים שמגיעים מבחוץ, השאירו את המפתחות כ-string ובדקו אותם.

keyof ב-Mapped Types

keyof הוא גם הדרך לעבור בלולאה על המפתחות של טיפוס כדי לבנות טיפוס חדש. כך כתובים טיפוסי העזר המובנים Partial ו-Readonly:

type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };

interface User {
  id: number;
  name: string;
}

type UserDraft = MyPartial<User>; // { id?: number; name?: string }

[K in keyof T] עובר על כל מפתח, ו-T[K] קורא את טיפוס המאפיין במפתח הזה. ל-mapped types יש עמוד משלהם, עם modifiers ו-key remapping.

שאלות נפוצות

מה keyof עושה ב-TypeScript?

keyof T מייצר union של שמות המאפיינים של הטיפוס T. עבור interface User { id: number; name: string }, keyof User הוא "id" | "name". הוא עובד רק על טיפוסים, בזמן קומפילציה, ולא מייצר שום JavaScript.

מה זה keyof typeof ב-TypeScript?

keyof צריך טיפוס, אבל אובייקט רגיל כמו const colors = { red: "#f00" } הוא ערך. typeof colors הופך את הערך לטיפוס שלו, ואז keyof typeof colors נותן את ה-union של המפתחות שלו, כאן "red". כתיבת keyof colors נכשלת עם השגיאה TS2749.

למה keyof מחזיר string | number?

כשלטיפוס יש string index signature כמו { [key: string]: number }. JavaScript ממיר מפתחות מספריים של מאפיינים למחרוזות, ולכן גם obj[42] היא גישה תקינה, ו-TypeScript כולל את number בטיפוס המפתח. השתמשו ב-Extract<keyof T, string> או ב-string & keyof T אם אתם רוצים רק את מפתחות המחרוזת.

למה Object.keys מחזיר string[] ולא (keyof T)[]?

כי אובייקט יכול להכיל יותר מאפיינים ממה שהטיפוס שלו מפרט: ערך עם שדה נוסף ניתן להשמה לטיפוס צר יותר. טיפוס כמו (keyof T)[] לתוצאה היה שקר עבור אובייקטים כאלה. אם אתם יודעים שלאובייקט יש בדיוק את המפתחות שהוצהרו, עשו cast: Object.keys(obj) as (keyof typeof obj)[].

איך מתקנים את "Element implicitly has an 'any' type because expression of type 'string' can't be used to index type"?

האינדקס הוא string רגיל, ולטיפוס האובייקט אין string index signature. תנו לפרמטר את הטיפוס keyof YourType במקום string, או בדקו את המחרוזת קודם עם type guard שמצמצם אותה ל-keyof YourType, או תנו לאובייקט index signature או טיפוס Record<string, V> אם הוא באמת מקבל כל מפתח.

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

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

להתחיל