Partial<T> הוא utility type מובנה שהופך כל מאפיין של T לאופציונלי. זה הטיפוס הטבעי לעדכון או ל-patch: מי שקורא לפונקציה שולח רק את השדות שהשתנו.
Partial<User> הוא { id?: number; name?: string; email?: string }. הקומפיילר עדיין בודק את השדות שכן מעבירים: { nmae: "x" } או { name: 42 } הם שגיאה, ובזכות זה Partial עדיף על פרמטר רופף מסוג object או any.
איך Partial מוגדר
Partial הוא mapped type של שורה אחת בספרייה הסטנדרטית של TypeScript:
type Partial<T> = {
[P in keyof T]?: T[P];
};
לכל מפתח P של T, הוא מצהיר על מאפיין אופציונלי עם אותו טיפוס. מכיוון שהוא ממפה על keyof T, הוא שומר על readonly במאפיינים שהיה להם. כטיפוס רגיל, אין לו שום השפעה בזמן ריצה: האובייקט שמועבר כ-changes הוא אותו אובייקט בכל מקרה.
קריאה מ-Partial נותנת T | undefined
כל מאפיין של Partial<T> יכול להיות חסר, ולכן קריאה שלו נותנת את טיפוס המאפיין ועוד undefined. הקומפיילר מחייב אתכם לטפל במקרה החסר:
{ ...defaults, ...opts } היא הדרך הרגילה להפוך Partial<Options> בחזרה ל-Options שלם: פורשים קודם את ברירות המחדל ונותנים לערכים שהועברו לדרוס אותן.
המלכודת של undefined מפורש
מאפיין אופציונלי יכול להיות חסר, אבל הוא יכול גם להיות קיים עם הערך undefined. object spread מעתיק את ה-undefined הזה על הערך האמיתי, וטיפוס התוצאה לא מראה את זה:
זה נושך כש-patch נבנה מטופס או מ-query string שבהם שדות ריקים הופכים ל-undefined. אפשרות הקומפיילר exactOptionalPropertyTypes (לא חלק מ-strict) הופכת את { name: undefined } לשגיאת קומפילציה עבור name?: string, אלא אם כותבים name?: string | undefined, וכך מונעת את הבעיה במקור.
Partial הוא רדוד
Partial הופך לאופציונליים רק את המאפיינים ברמה העליונה. אובייקט מקונן, אם מעבירים אותו, חייב להיות שלם:
index.ts(8,3): error TS2741: Property 'tabSize' is missing in type '{ fontSize: number; }' but required in type '{ fontSize: number; tabSize: number; }'.
המאפיין editor אופציונלי, אבל ברגע שהוא קיים הטיפוס שלו הוא { fontSize: number; tabSize: number }, בלי שינוי. לאובייקטי הגדרות, ל-patches של API ול-fixtures של בדיקות, לעתים קרובות רוצים מאפיינים אופציונליים בכל רמה. לשם כך צריך טיפוס רקורסיבי.
DeepPartial: גרסה רקורסיבית של Partial
ל-TypeScript אין גרסה עמוקה מובנית, אבל זה כמה שורות. פונקציות ומערכים נשארים כמו שהם, כי הפיכת איברי מערך לאופציונליים הייתה מתירה [undefined]:
הטיפוס רקורסיבי; המיזוג לא. applySettings ממזג את editor ידנית כי גם object spread הוא רדוד. פונקציית deep merge גנרית קיימת בספריות כמו lodash (merge), והגדרת הטיפוסים שלה קשה יותר מהטיפוס שלמעלה.
Required: ההפך של Partial
Required<T> מסיר את ה-? מכל מאפיין. הוא מוגדר עם המגדיר -?, שגם מסיר undefined מהטיפוס של כל מאפיין:
הדפוס זהה לזה של Partial, רק הפוך: המשתמשים ב-API מעבירים הגדרות רופפות, והקוד שבפנים עובד עם גרסת Required שבה ידוע שכל ערך קיים. ל-spread יש אותו חור כמו בפונקציית העדכון שלמעלה: מי שמעביר port: undefined במפורש דורס את ברירת המחדל ב-undefined, והקומפיילר מקבל את זה אלא אם exactOptionalPropertyTypes מופעל. Required רדוד בדיוק כמו Partial.
הפיכת רק חלק מהמאפיינים לאופציונליים או לחובה
Partial ו-Required חלים על כל מאפיין. כדי לשנות רק כמה, פצלו את הטיפוס עם Pick ו-Omit והרכיבו אותו מחדש:
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
interface Post {
id: number;
title: string;
body?: string;
}
type NewPost = PartialBy<Post, "id">; // id optional, title required, body optional
type Published = RequiredBy<Post, "body">; // body now required
| טיפוס | השפעה | עמוק? |
|---|---|---|
Partial<T> | כל מאפיין אופציונלי | לא |
Required<T> | כל מאפיין חובה, undefined מוסר | לא |
DeepPartial<T> (שלכם) | אופציונלי בכל רמה | כן |
PartialBy<T, K> (שלכם) | רק המפתחות K אופציונליים | לא |
Readonly<T> | כל מאפיין readonly | לא |
שאלות נפוצות
מה Partial עושה ב-TypeScript?
Partial<T> יוצר טיפוס עם כל המאפיינים של T, כשכולם מסומנים כאופציונליים. עבור interface User { name: string; email: string }, Partial<User> הוא { name?: string; email?: string }, ולכן {}, { name: "Ada" } ומשתמש מלא הם כולם ערכים תקינים.
האם Partial עמוק ב-TypeScript?
לא, Partial משפיע רק על המאפיינים ברמה העליונה. אובייקט מקונן בתוך Partial<T> עדיין חייב להיות שלם. לגרסה רקורסיבית, כתבו טיפוס DeepPartial<T> שמפעיל את עצמו על מאפיינים מטיפוס אובייקט.
מה ההפך של Partial ב-TypeScript?
Required<T>. הוא מסיר את ה-? מכל מאפיין וגם מסיר undefined מהטיפוסים שלהם, כך ש-Required<{ port?: number }> הוא { port: number }. הוא מוגדר כ-mapped type עם המגדיר -?.
איך הופכים רק חלק מהמאפיינים לאופציונליים?
משלבים Omit, Pick ו-Partial: type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>. PartialBy<User, "email"> משאיר כל מאפיין כמו שהיה, חוץ מ-email, שהופך לאופציונלי.
למה מאפיין הוא undefined אחרי מיזוג של עדכון Partial?
Partial<T> מתיר למאפיין להיות קיים עם הערך undefined, ו-object spread מעתיק אותו: { ...user, ...{ name: undefined } } מכיל name: undefined למרות ש-TypeScript נותן לתוצאה את הטיפוס User. סננו ערכי undefined לפני המיזוג, או הפעילו את exactOptionalPropertyTypes כדי ש-undefined מפורש יידחה.