Menu

Class ב-TypeScript: שדות, מתודות, getters ו-implements

מחלקות ב-TypeScript הן מחלקות JavaScript עם שדות, מתודות ובנאים שיש להם טיפוסים. כאן תלמדו איך עובדות הצהרות שדות ו-strictPropertyInitialization, איך נותנים טיפוס ל-this, על getters ו-setters, איברים סטטיים, implements, ואיך מחלקה משמשת גם כטיפוס.

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

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

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

הצהרה על שדות

שדה צריך הצהרה בגוף המחלקה לפני שהבנאי יכול להשים לו ערך. השמה ל-this.owner בלי להצהיר על owner היא השגיאה TS2339 (המאפיין לא קיים). שדה עם ערך התחלתי מקבל טיפוס שמוסק מהערך, בדיוק כמו משתנה.

הצהרהמשמעות
name: string;חייב לקבל ערך בבנאי
count = 0;מאותחל, הטיפוס מוסק כ-number
label?: string;אופציונלי, הטיפוס הוא string | undefined
readonly id: number;מקבל ערך פעם אחת, ואז לקריאה בלבד
data!: string[];definite assignment: סמכו עליי, הוא יקבל ערך
static total = 0;שייך למחלקה, לא למופעים

כש-strict פעיל, strictPropertyInitialization בודק שכל שדה שאינו אופציונלי מקבל ערך. המחלקה הזו שוכחת שניים:

הקומפיילר מדווח על שניהם:

index.ts(3,5): error TS2564: Property 'name' has no initializer and is not definitely assigned in the constructor.
index.ts(4,5): error TS2564: Property 'age' has no initializer and is not definitely assigned in the constructor.

הבדיקה קיימת כי בלעדיה new Profile().name.toUpperCase() היה עובר בדיקת טיפוסים ואז קורס עם undefined. תקנו כל שדה באחת מארבע דרכים: תנו לו ערך התחלתי, השימו לו ערך בבנאי, סמנו אותו כאופציונלי עם ?, או כתבו name!: string כש-framework או מתודת init() מציבים את הערך מאוחר יותר. הדרך האחרונה מכבה את הבדיקה לשדה הזה, אז השתמשו בה במשורה.

מתודות ו-this

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

class QueryBuilder {
    private parts: string[] = [];

    where(condition: string): this {
        this.parts.push(condition);
        return this;
    }

    build(): string {
        return this.parts.join(" AND ");
    }
}

const sql = new QueryBuilder().where("age > 18").where("active = 1").build();
console.log(sql); // age > 18 AND active = 1

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

שדה עם פונקציית חץ עובד כי כל מופע מקבל פונקציה משלו שסוגרת על this. המחיר הוא פונקציה אחת לכל מופע במקום מתודה משותפת אחת על ה-prototype.

Getters ו-Setters

accessors של get ו-set נראים מבחוץ כמו מאפיינים. getter בלי setter הוא לקריאה בלבד: השמה אליו היא שגיאת קומפילציה (TS2540).

הקומפיילר דוחה את ההשמה, ואם עוקפים את הבדיקה, גם זמן הריצה זורק TypeError, כי הקובץ המקומפל רץ ב-strict mode (הקומפיילר כותב "use strict" בראש הקובץ). ה-RangeError ב-setter הוא בדיקת זמן ריצה שכתבתם בעצמכם. הטיפוס number רק אומר שהערך הוא מספר; הוא לא יודע כלום על טווחים.

איברים סטטיים

שדות ומתודות static שייכים למחלקה עצמה. ניגשים אליהם דרך שם המחלקה, לא דרך מופע. בלוק static { } רץ פעם אחת כשהמחלקה מוגדרת.

איברים סטטיים לא יכולים להשתמש בפרמטרי הטיפוס של המחלקה: ב-class Box<T>, static empty: T הוא השגיאה TS2302, כי יש רק שדה סטטי אחד, שמשותף לכל Box<string>, Box<number> וכן הלאה.

מימוש interface

implements מבקש מהקומפיילר לבדוק שלמחלקה יש כל מה ש-interface דורש. איבר חסר הוא השגיאה TS2420: Class 'X' incorrectly implements interface 'Y'.

מחלקה יכולה לממש כמה interfaces: class Doc implements Printable, Serializable. יש שני דברים ש-implements לא עושה. הוא לא נותן טיפוסים לפרמטרים של המתודות במחלקה בשבילכם: check(s) { ... } בתוך מחלקה שמממשת check(s: string): boolean הוא עדיין השגיאה TS7006 (Parameter 's' implicitly has an 'any' type), אז תנו להם הערות טיפוס. והוא לא משאיר שום זכר בזמן ריצה, ולכן obj instanceof Shape לא אפשרי.

ה-private radius בבנאי הוא parameter property: הוא מצהיר על השדה ומשים לו ערך בצעד אחד. ראו בנאים.

מחלקה היא גם טיפוס

הצהרה על מחלקה יוצרת שני דברים עם שם אחד: ערך (פונקציית הבנאי שקוראים לה עם new) וטיפוס (המבנה של מופע). מכיוון ש-TypeScript משווה טיפוסים לפי מבנה, כל אובייקט עם אותם איברים ציבוריים מתאים לטיפוס, גם אם הוא לא נוצר על ידי המחלקה.

האובייקט הרגיל עובר את בדיקת הטיפוסים אבל הוא לא Point בזמן ריצה: ({ x: 6, y: 8 }) instanceof Point הוא false. מחלקה עם איבר private או #private מונעת את זה: רק מופעים של המחלקה הזו (או של תתי-המחלקות שלה) ניתנים להשמה אליה.

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

  • הצהרה על שדה בלי לתת לו ערך אף פעם. TS2564 אומרת לכם שהשדה יהיה undefined. אתחלו אותו במקום להשתיק את השגיאה עם !.
  • העברת מתודה כ-callback. button.onclick = obj.handle מאבד את this. השתמשו בשדה עם פונקציית חץ או ב-obj.handle.bind(obj).
  • ציפייה ש-implements יוסיף קוד או טיפוסים. הוא רק בודק. טיפוסי הפרמטרים עדיין צריכים הערות טיפוס.
  • שימוש ב-instanceof עם interface. interfaces לא קיימים בזמן ריצה; בדקו מול מחלקה או השתמשו ב-type guard.
  • הנחה שטיפוסים מבצעים ולידציה למידע. שדה number מקבל כל מספר בזמן קומפילציה, וכל דבר בכלל ממידע בלי טיפוסים בזמן ריצה.

שאלות נפוצות

איך יוצרים מחלקה ב-TypeScript?

מצהירים על כל שדה עם הטיפוס שלו בגוף המחלקה, ואז כותבים את הבנאי והמתודות כמו ב-JavaScript: class User { name: string; constructor(name: string) { this.name = name; } greet(): string { return "Hi, " + this.name; } }. יוצרים מופע עם new User("Ada").

מה המשמעות של "has no initializer and is not definitely assigned in the constructor"?

זו השגיאה TS2564 של strictPropertyInitialization (חלק מ-strict). שדה מטיפוס string היה מתחיל כ-undefined כי שום דבר לא מציב בו ערך. תקנו אותה עם ערך התחלתי (name = ""), עם השמה בבנאי, עם שדה אופציונלי (name?: string), או, כשמשהו מחוץ לבנאי מציב את הערך, עם definite assignment assertion (name!: string).

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

extends יורש קוד ממחלקת אב: השדות והמתודות שלה קיימים במחלקת הבת בזמן ריצה. implements רק מבקש מהקומפיילר לבדוק שלמחלקה יש את המבנה של interface. הוא לא מוסיף כלום למחלקה ונעלם מה-JavaScript המקומפל.

האם אפשר להשתמש במחלקה כטיפוס ב-TypeScript?

כן. שם של מחלקה הוא גם ערך (הבנאי) וגם טיפוס (המבנה של המופעים שלה). let u: User מקבל כל אובייקט עם אותם איברים ציבוריים, כי TypeScript משווה טיפוסים לפי מבנה. typeof User הוא הטיפוס של הבנאי עצמו.

האם מחלקות TypeScript שונות ממחלקות JavaScript בזמן ריצה?

לא. TypeScript מקמפלת מחלקה למחלקת JavaScript רגילה. הערות טיפוס, פסוקיות implements ומגדירים כמו private נמחקים; רק תכונות של JavaScript כמו שדות #private, בלוקי static ו-getters קיימות בזמן ריצה.

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

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

להתחיל