mapped type בונה טיפוס אובייקט חדש באמצעות מעבר על קבוצת מפתחות. { [K in keyof T]: boolean } פירושו "לכל מפתח K של T, מאפיין בשם K מטיפוס boolean":
הוסיפו מפתח ל-Features ו-Flags<Features> ידרוש גם אותו. mapped types קיימים רק בזמן קומפילציה: הם מתארים אובייקטים, הם לא יוצרים אותם.
התחביר
ל-mapped type יש שלושה חלקים: משתנה מפתח, union של מפתחות לעבור עליהם, וטיפוס המאפיין, שיכול להשתמש במשתנה המפתח.
type MappedType = {
[K in Keys]: PropertyType; // K takes each member of Keys in turn
};
Keys יכול להיות כל union של מחרוזות, מספרים או symbols. הוא לא חייב להגיע מ-keyof:
זה בדיוק מה ש-Record<Size, number> עושה; Record הוא mapped type בשורה אחת. טיפוס המאפיין יכול להשתמש במפתח: { [K in keyof T]: T[K] } מעתיק כל טיפוס מאפיין דרך indexed access, ו-{ [K in keyof T]: T[K] | null } הופך כל מאפיין ל-nullable.
מודיפיירים: readonly ו-? עם + ו--
mapped type יכול להוסיף או להסיר את המודיפיירים readonly ואופציונלי (?) מכל המאפיינים בבת אחת. הוסיפו + כקידומת כדי להוסיף (ברירת המחדל כשלא כותבים כלום) ו-- כדי להסיר:
השורה האחרונה מראה ש-readonly הוא רק כלל של זמן קומפילציה: ההשמה דווחה (והושתקה כאן עם @ts-expect-error), אבל ה-JavaScript שנוצר עדיין הריץ אותה. -? עושה יותר מלהסיר את סימן השאלה: הוא גם מסיר את undefined מטיפוס המאפיין, כך ש-AllRequired<Account> דוחה את { id: 1, email: undefined } עם error TS2322: Type 'undefined' is not assignable to type 'string'.
איך בנויים Partial, Readonly, Pick ו-Record
טיפוסי העזר המובנים שמעצבים מחדש אובייקטים הם mapped types. אלה ההגדרות מתוך lib.es5.d.ts של TypeScript עצמו:
type Partial<T> = { [P in keyof T]?: T[P] };
type Required<T> = { [P in keyof T]-?: T[P] };
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Record<K extends keyof any, T> = { [P in K]: T };
קריאה שלהן היא תרגול טוב: Pick עובר רק על המפתחות שמעבירים לו, ו-Record מתעלם מכל טיפוס מקור ונותן לכל מפתח את אותו טיפוס ערך. Omit אינו mapped type בפני עצמו: הוא Pick<T, Exclude<keyof T, K>>. המדריך ל-utility types מפרט את כולם.
מיפוי מפתחות מחדש עם as
פסוקית as אחרי המפתח משנה את שם המאפיין. בשילוב עם template literal types היא יכולה לייצר שמות חדשים מהישנים:
string & K נמצא שם כי keyof T עשוי לכלול מפתחות מסוג number ו-symbol, ו-Capitalize מקבל רק מחרוזות. ה-intersection שומר את מפתחות המחרוזת ומשמיט את השאר.
סינון מפתחות עם never
אם פסוקית as מייצרת never עבור מפתח, המפתח הזה מוסר. עם conditional type אפשר לשמור או להשמיט מאפיינים לפי הטיפוס שלהם:
טיפוסים כמו DataOnly שימושיים כדי לתאר מה שורד את JSON.stringify או מה טופס עורך: השדות בלי המתודות.
אילו מודיפיירים נשמרים
mapped type על keyof T עבור טיפוס T כלשהו נקרא homomorphic, והוא מעתיק את המודיפיירים readonly ו-? של כל מאפיין מ-T. mapped type על union פשוט של מפתחות מתחיל בלי מודיפיירים:
type Account = { readonly id: number; email?: string };
type Copy<T> = { [K in keyof T]: T[K] };
type A = Copy<Account>;
// { readonly id: number; email?: string } (modifiers kept)
type B = { [K in "id" | "email"]: Account[K] };
// { id: number; email: string | undefined } (email is required now)
ב-B עדיין יש undefined בטיפוס של email, כי Account["email"] כולל אותו, אבל המאפיין עצמו כבר לא אופציונלי: { id: 1 } נדחה. זו הסיבה ש-Partial<T> וחבריו ממפים על keyof T: הם שומרים על כל מה שהם לא משנים במפורש.
mapped type גנרי homomorphic שמופעל על מערך או tuple מייצר מערך או tuple, לא אובייקט עם מפתחות מספריים. Readonly<string[]> הוא readonly string[], ועם type Box<T> = { [K in keyof T]: { value: T[K] } }, Box<[string, number]> הוא [{ value: string }, { value: number }].
Mapped types וקוד בזמן ריצה
מאחר ש-mapped type רק מתאר אובייקט, פונקציה שבונה אובייקט כזה צריכה לולאה משלה בזמן ריצה. הטיפוס מתאר אז את התוצאה שלה:
ה-callbacks מקבלים את טיפוסי הפרמטרים שלהם מה-mapped type (v הוא string באחד ו-number בשני) בלי שום הגדרת טיפוס. אם הגעתם לכאן בחיפוש אחר המחלקה Map ולא אחר מיפוי טיפוסים, ראו Map ב-TypeScript.
שאלות נפוצות
מה זה mapped type ב-TypeScript?
טיפוס שיוצר טיפוס אובייקט על ידי מעבר על union של מפתחות: { [K in Keys]: SomeType }. לרוב המפתחות הם keyof T, כך שלטיפוס החדש יש אותם מפתחות כמו ל-T עם טיפוסי מאפיינים שעברו טרנספורמציה, כמו ב-type Flags<T> = { [K in keyof T]: boolean }.
מה המשמעות של +readonly, -readonly, +? ו--? ב-mapped type?
הם מוסיפים או מסירים מודיפיירים. readonly או +readonly הופכים כל מאפיין ל-readonly, -readonly מסיר את ה-readonly. ? או +? הופכים כל מאפיין לאופציונלי, -? הופך אותם לחובה וגם מסיר את undefined מהטיפוסים שלהם. Required<T> כתוב עם -?.
איך משנים שמות של מפתחות ב-mapped type?
השתמשו בפסוקית as אחרי המפתח: { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] } הופך את name ל-getName. מיפוי מפתח ל-never בפסוקית as מסיר אותו, וכך מסננים מאפיינים.
האם mapped type זהה ל-Map ב-TypeScript?
לא. mapped type הוא טרנספורמציה של טיפוסים בזמן קומפילציה ולא מייצר שום קוד. Map<K, V> הוא מחלקת Map של JavaScript, אוסף של זוגות מפתח וערך בזמן ריצה. חיפושים של "typescript map type" מתכוונים לעיתים קרובות לאחד מהשניים.
איך ממומשים Partial ו-Readonly?
כ-mapped types בספרייה הסטנדרטית: type Partial<T> = { [P in keyof T]?: T[P] } ו-type Readonly<T> = { readonly [P in keyof T]: T[P] }. גם Pick ו-Record הם mapped types; Omit הוא Pick בשילוב עם Exclude.