Menu

TypeScript Partial ו-Required: דוגמאות ו-Deep Partial

Partial<T> הופך כל מאפיין של T לאופציונלי, וזה בדיוק הטיפוס של אובייקט עדכון או patch. למדו את Partial בפונקציות עדכון, למה הוא רדוד, איך כותבים DeepPartial, המלכודת של undefined מפורש, ואת ההפך שלו, Required<T>.

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

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 מפורש יידחה.

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

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

להתחיל