Menu

ירושה ב-TypeScript: extends, super ו-override

ירושת מחלקות ב-TypeScript: extends ו-super, דריסת מתודות עם טיפוסים תואמים, מילת המפתח override ו-noImplicitOverride, איברים מוגנים, הצהרה מחדש על שדות עם declare, ומתי implements הוא הכלי המתאים יותר.

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

מחלקה ב-TypeScript יורשת ממחלקה אחרת עם extends. תת-המחלקה מקבלת כל שדה וכל מתודה של ההורה, קוראת לבנאי של ההורה עם super(...), ויכולה לדרוס מתודות, ו-TypeScript בודק שכל דריסה נשארת תואמת לטיפוסים של ההורה.

זו ירושת מחלקות של JavaScript (ראו ירושה ב-JavaScript על שרשרת ה-prototype שמאחוריה). TypeScript מוסיף את בדיקות הטיפוסים: אפשר להשים Dog בכל מקום שבו מצופה Animal, והקומפיילר מוודא את הדריסה ואת הקריאה ל-super.

דריסת מתודות

מתודה של תת-מחלקה עם אותו שם מחליפה את זו של ההורה. הטיפוס שלה חייב להיות ניתן להשמה לטיפוס של ההורה: פרמטרים זהים או תואמים, וטיפוס החזרה זהה או צר יותר. אחרת כל קוד שמחזיק Animal היה יכול לקרוא למתודה עם ארגומנטים שתת-המחלקה לא יודעת לטפל בהם.

שינוי הפרמטר ל-value: string היה השגיאה TS2416: Property 'format' in type 'CurrencyFormatter' is not assignable to the same property in base type 'Formatter'. הלולאה מראה למה: היא קוראת ל-format עם מספר על כל איבר, בלי לדעת איזו תת-מחלקה יש לה ביד.

יש חור אחד. פרמטרים של מתודות מושווים bivariantly, ולכן דריסה שמקבלת טיפוס צר יותר, כמו format(value: 1 | 2), מתקמפלת למרות שהלולאה עדיין יכולה להעביר לה 1234567. טיפוסי החזרה נבדקים בקפדנות: החזרה של number במקום string היא TS2416.

מילת המפתח override ו-noImplicitOverride

override אומרת "האיבר הזה מחליף איבר במחלקת הבסיס", והקומפיילר מחזיק אתכם לזה. אם אין בבסיס איבר כזה (שגיאת כתיב, או מתודה שמישהו שינה את שמה בהורה), זו שגיאה:

index.ts(9,14): error TS4117: This member cannot have an 'override' modifier because it is not declared in the base class 'Animal'. Did you mean 'speak'?

בלי override, שגיאת הכתיב הזו מתקמפלת ו-speak() מחזירה בשקט "...". מילת המפתח עוזרת רק איפה שהיא כתובה, אז הפעילו את noImplicitOverride ב-tsconfig.json. אז כל מתודה שדורסת איבר של הבסיס חייבת לציין זאת, ו-speak() רגילה בתת-מחלקה היא השגיאה TS4114 (This member must have an 'override' modifier because it overrides a member in the base class 'Animal').

{
    "compilerOptions": {
        "strict": true,
        "noImplicitOverride": true
    }
}

noImplicitOverride אינה חלק מ-strict; צריך להוסיף אותה בעצמכם. override נמחקת מה-JavaScript המקומפל.

איברים protected בתתי-מחלקות

איברים private של ההורה לא נראים בתת-המחלקה. איברים protected כן נראים, וזו הסיבה ש-protected קיים: זה ה-API שמחלקת הורה מציעה לילדים שלה.

ראו access modifiers לכללים המלאים, כולל למה לא private ולא protected נאכפים בזמן ריצה.

הצהרה מחדש על שדה עם טיפוס צר יותר

לעתים קרובות תת-מחלקה יודעת ששדה מחזיק משהו ספציפי יותר ממה שההורה אומר. להצהרה מחדש שלו כ-pet: Dog יש השפעה בזמן ריצה עם class fields מודרניים: השדה של תת-המחלקה מאותחל ל-undefined אחרי ש-super() חוזר, ומוחק את מה שהבנאי של ההורה שמר. אלא אם הבנאי של תת-המחלקה משים אותו שוב, TypeScript מדווח על כך כ-TS2612 (Property 'pet' will overwrite the base property in 'Home'). השתמשו ב-declare כדי לשנות רק את הטיפוס:

declare לא מייצר כלום, ולכן הערך שהבנאי של Home קבע נשמר. הבנאי שמקבל רק Dog הוא מה שהופך את הטיפוס הצר יותר לנכון.

implements מול extends

שתי מילות המפתח עונות על שאלות שונות, ומחלקה יכולה להשתמש בשתיהן.

extendsimplements
עובד עםמחלקה אחתכמה interfaces שרוצים (או type aliases של טיפוסי אובייקט)
מביא קודכן: שדות, מתודות, בנאילא, הוא רק בודק את המבנה
קיים בזמן ריצהכן, שרשרת ה-prototypeלא, נמחק
instanceof עובדכןלא
שימוש טיפוסישיתוף התנהגות אמיתיתהבטחת מבנה לשאר הקוד

מחלקה יכולה להרחיב רק מחלקה אחת (TS1174: Classes can only extend a single class). כדי לשלב התנהגות מכמה מקומות, ממשו כמה interfaces ושתפו קוד דרך קומפוזיציה (שדה שמחזיק אובייקט עזר) או mixins.

Mixins

mixin היא פונקציה שמקבלת מחלקה ומחזירה תת-מחלקה שלה. היא נותנת סוג של ירושה מרובה שעוברת בדיקת טיפוסים:

ה-any[] ב-Constructor הכרחי: TypeScript מקבל בסיס של mixin רק אם הבנאי שלו מקבל פרמטר rest יחיד מהטיפוס any[]. mixins עובדים, אבל הם מקשים על קריאת הטיפוסים ו-stack traces, אז העדיפו קודם קומפוזיציה.

טעויות נפוצות

  • קריאה למתודה שאפשר לדרוס מתוך הבנאי של הבסיס. הבנאי של הבסיס רץ לפני שהשדות של תת-המחלקה מאותחלים, ולכן דריסה שקוראת את this.items רואה undefined, וקריאה כמו this.items.push(x) זורקת TypeError בזמן ריצה. TypeScript לא תופס את זה.
  • שכחה של super(...) או קריאה מאוחרת לו. השגיאות TS2377 ו-TS17009; ההורה חייב לבנות את האובייקט לפני שתת-המחלקה נוגעת ב-this.
  • הצהרה מחדש על שדה של ההורה בלי declare. השדה של תת-המחלקה מאפס אותו ל-undefined.
  • היררכיות עמוקות. כל רמה קושרת את הילד לפרטים הפנימיים של ההורה. שתי רמות בדרך כלל מספיקות; מעבר לזה העדיפו קומפוזיציה או interfaces.

שאלות נפוצות

איך עובדת ירושה ב-TypeScript?

מחלקה יורשת ממחלקה אחרת עם extends: class Dog extends Animal. תת-המחלקה מקבלת את השדות והמתודות של ההורה, קוראת לבנאי של ההורה עם super(...), ויכולה לדרוס מתודות. TypeScript בודק שהדריסות נשארות תואמות לטיפוסים של ההורה.

מה עושה מילת המפתח override ב-TypeScript?

override מסמנת מתודה או מאפיין שמחליפים איבר ממחלקת הבסיס. אם למחלקת הבסיס אין איבר בשם הזה, הקומפיילר מדווח TS4117, וכך נתפסות שגיאות כתיב ואיברים ששמם שונה בהורה. כש-noImplicitOverride פועל, כל דריסה חייבת לשאת את מילת המפתח (אחרת TS4114).

איך קוראים למתודה של מחלקת ההורה ב-TypeScript?

משתמשים ב-super.methodName(...) בתוך המתודה של תת-המחלקה. בבנאי, super(...) קורא לבנאי של ההורה, והקריאה חייבת לרוץ לפני כל שימוש ב-this.

האם TypeScript תומך בירושה מרובה?

לא. מחלקה יכולה להרחיב רק מחלקה אחת (TS1174: Classes can only extend a single class). היא יכולה לממש כמה interfaces שרוצים, ו-mixins (פונקציות שמקבלות מחלקה ומחזירות תת-מחלקה) משלבים התנהגות מכמה מקורות.

מה ההבדל בין extends ל-implements?

extends יורש קוד אמיתי ממחלקת הורה. implements רק בודק שמחלקה מתאימה למבנה של interface, ולא מוסיף כלום בזמן ריצה. מחלקה יכולה לעשות את שניהם: class Dog extends Animal implements Pet.

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

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

להתחיל