interface Child extends Parent יוצר interface חדש עם כל האיברים של Parent ועם האיברים שלו עצמו. אפשר להשתמש בערכים מטיפוס הילד בכל מקום שבו מצופה ההורה, כי יש להם את כל המאפיינים שלו.
אותה מילת מפתח מופיעה ב-TypeScript בארבעה מקומות, עם משמעויות קרובות אבל שונות:
| איפה | דוגמה | משמעות |
|---|---|---|
| Interface | interface Dog extends Animal {} | ירושת האיברים של ההורה |
| מחלקה | class Dog extends Animal {} | ירושת שדות ומימושי מתודות (JavaScript בזמן ריצה) |
| פרמטר גנרי | <T extends Animal> | אילוץ: ארגומנט הטיפוס חייב להיות ניתן להשמה ל-Animal |
| Conditional type | T extends string ? A : B | בדיקה: האם T ניתן להשמה ל-string? |
הרחבה של כמה interfaces
כתבו כמה הורים אחרי extends, מופרדים בפסיקים. לתוצאה יש את האיברים של כולם:
interface Identified {
id: number;
}
interface Timestamped {
createdAt: Date;
updatedAt: Date;
}
interface Post extends Identified, Timestamped {
title: string;
}
// Post requires id, createdAt, updatedAt and title
אם שני הורים מצהירים על אותו מאפיין עם טיפוסים שאינם זהים, הילד הוא השגיאה TS2320 (Interface 'Post' cannot simultaneously extend types 'A' and 'B'), אלא אם הילד מצהיר מחדש על המאפיין הזה עם טיפוס שניתן להשמה לשניהם.
הרחבה של type alias
ל-type alias אין פסוקית extends. בונים עליו עם intersection, &, שמשלב את האיברים של שני הצדדים. גם interface יכול להרחיב ישירות type alias של אובייקט, ו-type alias יכול ליצור intersection עם interface: שני הסוגים משתלבים בחופשיות.
שתי הדרכים מתנהגות אותו דבר עד שיש התנגשות בין מאפיינים: extends מדווח על מאפיין מתנגש כבר בהצהרה, ואילו & הופך אותו בשקט ל-never. ההשוואה נמצאת בעמוד interface מול type, והפרטים על & נמצאים בעמוד על intersection types.
דריסה של טיפוסי מאפיינים
interface ילד יכול להצהיר מחדש על מאפיין של ההורה, אבל רק עם טיפוס שניתן להשמה לטיפוס של ההורה. מותר לצמצם string לליטרל, או להסיר null מ-union. אסור לשנות את הטיפוס למשהו שאינו קשור:
index.ts(13,11): error TS2430: Interface 'Label' incorrectly extends interface 'Shape'.
Types of property 'kind' are incompatible.
Type 'number' is not assignable to type 'string'.
הכלל קיים כדי שעדיין יהיה אפשר להשתמש ב-Label בכל מקום שבו מצופה Shape. אם kind היה יכול להיות מספר, קוד שנכתב עבור Shape וקורא ל-shape.kind.toUpperCase() היה נשבר.
החלפת מאפיינים עם Omit
כשבאמת צריך טיפוס אחר למאפיין שעובר בירושה, הסירו אותו קודם מההורה עם Omit, ואז הצהירו על המאפיין החדש. מקרה נפוץ הוא המרה של נתונים מ-API, שבהם מזהים ותאריכים מגיעים כמחרוזות:
Omit<ApiUser, "id" | "createdAt"> הוא { name: string }, ולכן id ו-createdAt החדשים לא מתנגשים בשום דבר. אפשר להרחיב באותה דרך גם את Pick, את Partial ואת שאר ה-utility types.
extends עם מחלקות
class Dog extends Animal הוא ירושת מחלקות של JavaScript, ובניגוד ל-extends של interface הוא קיים בזמן ריצה: Dog מקבל את הבנאי, את השדות ואת מימושי המתודות של Animal, ו-new Dog(...) instanceof Animal הוא true. TypeScript מוסיף מעל זה בדיקות טיפוסים, כמו מילת המפתח override:
מחלקה יכולה לעשות extends רק למחלקה אחת, אבל היא יכולה לעשות implements לכמה interfaces שרוצים. implements לא מעתיק שום דבר: הוא רק בודק שהמחלקה מצהירה על מה שה-interface דורש. super, איברים מוגנים ו-override מוסברים בעמוד על ירושה.
extends בגנריקס וב-conditional types
בתוך סוגריים משולשים, extends הוא אילוץ. <T extends { length: number }> מקבל כל ארגומנט טיפוס שיש לו length מספרי, ומאפשר לגוף הפונקציה להשתמש ב-.length. ב-conditional type, T extends U ? X : Y שואל האם T ניתן להשמה ל-U.
בשני המקומות extends אומר "ניתן להשמה ל", אותו יחס שצורת ה-interface בודקת. אילוצים מוסברים לעומק בעמוד על אילוצים גנריים, ובדיקות ברמת הטיפוסים בעמוד על conditional types.
שאלות נפוצות
איך מרחיבים interface ב-TypeScript?
כותבים interface Child extends Parent { ... }. לממשק הילד יש כל מאפיין של ההורה, ובנוסף המאפיינים שהוא מצהיר עליהם. כדי להרחיב כמה interfaces בבת אחת, מפרידים ביניהם בפסיקים: interface C extends A, B { ... }.
איך מרחיבים type alias ב-TypeScript?
type alias לא יכול להשתמש ב-extends; במקום זה משלבים טיפוסים עם intersection: type Admin = User & { permissions: string[] }. גם interface יכול להרחיב ישירות type alias של אובייקט: interface Admin extends User { permissions: string[] }.
אפשר לדרוס את הטיפוס של מאפיין כשמרחיבים interface?
רק בטיפוס תואם וצר יותר: kind: "circle" יכול להחליף את kind: string. טיפוס אחר, כמו number במקום מאפיין string, הוא השגיאה TS2430 (Interface 'X' incorrectly extends interface 'Y'). כדי להחליף מאפיין בטיפוס שאינו קשור, הסירו אותו קודם: interface User extends Omit<ApiUser, "id"> { id: number }.
מה ההבדל בין extends ל-implements?
extends יורש: interface מקבל את האיברים של ההורה, ומחלקה מקבלת את השדות ואת מימושי המתודות של מחלקת ההורה. implements רק בודק: מחלקה שמממשת interface חייבת להצהיר בעצמה על כל איבר, והיא לא מקבלת שום דבר מה-interface.
מה המשמעות של extends בגנרי כמו <T extends string>?
זה אילוץ, לא ירושה. T extends string אומר שארגומנט הטיפוס חייב להיות ניתן להשמה ל-string, ולכן בתוך הפונקציה אפשר להשתמש במתודות של מחרוזות על T. ב-conditional type, T extends U ? X : Y הוא בדיקה: אם T ניתן להשמה ל-U, התוצאה היא X, אחרת Y.