interface נותן שם למבנה של אובייקט: המאפיינים שחייבים להיות לו והטיפוס של כל אחד. אחרי ההצהרה משתמשים בשם כטיפוס, והקומפיילר בודק מולו כל אובייקט שמעבירים, מחזירים או משימים.
הקריאה האחרונה היא שגיאת הקומפילציה TS2741. interfaces נמחקים כשהקוד מתקמפל: בפלט ה-JavaScript אין שום זכר ל-User, ושום דבר לא בודק את המבנה בזמן ריצה.
הצהרה על interface
התחביר הוא מילת המפתח interface, שם (לפי המוסכמה ב-PascalCase), וגוף שמפרט איברים. אפשר להפריד בין איברים בנקודה-פסיק, בפסיקים או סתם בשורות חדשות; נקודה-פסיק היא הסגנון הנפוץ.
interface Product {
sku: string; // required property
price: number;
tags: string[]; // array property
dimensions: { // nested object type
width: number;
height: number;
};
discount?: number; // optional property
readonly createdAt: Date; // cannot be reassigned
label(): string; // method
}
interface הוא טיפוס, לא ערך. אי אפשר ליצור ממנו מופע עם new, אין לו ערכי ברירת מחדל, ו-obj instanceof Product היא השגיאה TS2693 ('Product' only refers to a type, but is being used as a value here). כדי לבדוק מבנה בזמן ריצה, כתבו type guard.
טיפוסיות מבנית ובדיקת מאפיינים עודפים
TypeScript משווה מבנים, לא שמות. כל אובייקט עם המאפיינים הנדרשים מתאים ל-interface, בין אם הוא הוצהר איתו ובין אם לא. מאפיינים נוספים מותרים, עם חריג אחד: ליטרל אובייקט שנכתב ישירות במקום שבו מצופה ה-interface עובר בדיקת מאפיינים עודפים, כי מפתח לא מוכר שם הוא כמעט תמיד שגיאת כתיב.
השגיאה הזו (TS2353) היא מה שתופס את { id: 1, name: "a", emial: "x" } עבור User: הקומפיילר אפילו מציע Did you mean to write 'email'? (TS2561).
מאפיינים אופציונליים ו-readonly
? אחרי השם הופך מאפיין לאופציונלי: האובייקט יכול להשמיט אותו, וקריאה שלו נותנת T | undefined. readonly אוסר להשים מחדש את המאפיין אחרי שהאובייקט נוצר.
כאן רואים שתי מגבלות. ראשית, readonly קיים רק בזמן הקומפילציה: שתי השורות שמסומנות ב-@ts-expect-error עדיין רצות כשלוחצים על Run, והן מצליחות, והשורות האחרונות משנות את apiUrl דרך הפניה שהטיפוס שלה בלי readonly. שנית, הוא רדוד: readonly hosts: string[] היה מונע השמה מחדש של hosts אבל עדיין מאפשר hosts.push(...), ולכן המערך עצמו מקבל את הטיפוס readonly string[]. הוא מתעד ואוכף כוונה בקוד עם טיפוסים; הוא לא מקפיא שום דבר. ה-utility type Readonly<T> הופך בבת אחת כל מאפיין של interface קיים ל-readonly.
מתודות ומאפיינים של פונקציות
אפשר לכתוב מתודה כחתימת מתודה, name(params): ReturnType, או כמאפיין שמחזיק פונקציה, name: (params) => ReturnType. מי שקורא להן משתמש בשתיהן באותה דרך.
ההבדל עדין: תחת strictFunctionTypes (חלק מ-strict), פרמטרים של מאפיינים מטיפוס פונקציה נבדקים בקפדנות, ואילו חתימות מתודה נבדקות בצורה רופפת יותר (bivariantly), ולכן צורת המאפיין תופסת עוד כמה טעויות. תחביר המתודה קצר יותר והוא הסגנון הנפוץ יותר; שניהם בסדר.
interface יכול גם לתאר משהו שאפשר לקרוא לו או לבנות אותו, עם חתימת קריאה או חתימת בנייה:
interface Formatter {
(value: number): string; // call signature: the object is a function
locale: string; // and it also has a property
}
interface PointConstructor {
new (x: number, y: number): { x: number; y: number }; // construct signature
}
Index signatures
כששמות המאפיינים לא ידועים מראש, index signature מתארת את כולם בבת אחת: [key: string]: T אומר "כל מפתח מחרוזת, וכל אחד מחזיק T".
השורות האחרונות מראות את המלכודת: קריאה של מפתח שלא קיים מקבלת את הטיפוס number, לא number | undefined. אפשרות הקומפיילר noUncheckedIndexedAccess מוסיפה | undefined לכל קריאה כזו.
מאפיינים עם שם יכולים לשבת ליד index signature, אבל הם חייבים להתאים לה. interface Dict { [key: string]: number; name: string } היא השגיאה TS2411, Property 'name' of type 'string' is not assignable to 'string' index type 'number'. הרחיבו את טיפוס האינדקס ([key: string]: number | string) או העבירו את החלק הדינמי למאפיין משלו. למפות פשוטות של מפתח וערך, Record<string, number> אומר את אותו דבר בשורה אחת.
הרחבה של interface
extends בונה interface חדש מתוך interface קיים אחד או יותר. לילד יש כל איבר של ההורה, ובנוסף האיברים שלו:
interface Animal {
name: string;
}
interface Pet extends Animal {
owner: string;
}
interface Trained {
commands: string[];
}
interface ServiceDog extends Pet, Trained {
certifiedUntil: Date;
}
// ServiceDog requires: name, owner, commands, certifiedUntil
ילד יכול להצהיר מחדש על מאפיין של ההורה רק עם טיפוס תואם (צר יותר), כמו kind: "dog" כשההורה אומר kind: string. הכללים, ואיך מרחיבים type aliases, נמצאים בעמוד על extends.
מימוש interface במחלקה
class X implements Shape מבקש מהקומפיילר לבדוק שיש למחלקה את כל מה שה-interface דורש. איבר חסר הוא שגיאה בהצהרת המחלקה:
index.ts(7,7): error TS2420: Class 'Circle' incorrectly implements interface 'Shape'.
Property 'area' is missing in type 'Circle' but required in type 'Shape'.
אחרי שמוסיפים את area(), כמה מחלקות ואפילו אובייקט רגיל יכולים כולם לשמש כ-Shape:
implements הוא רק בדיקה. הוא לא מוסיף איברים למחלקה, והוא לא נותן בשבילכם טיפוסים לפרמטרים של המתודות במחלקה: greet(name) {} בתוך מחלקה שמממשת greet(name: string): string היא עדיין השגיאה TS7006, Parameter 'name' implicitly has an 'any' type. מחלקה יכולה לממש כמה interfaces: class A implements B, C.
Declaration merging
הצהרה על interface עם אותו שם פעמיים באותו scope ממזגת את השתיים לאחת. זה משהו ש-type aliases לא יכולים לעשות (type שני עם אותו שם הוא שגיאת duplicate identifier).
interface Settings {
theme: string;
}
interface Settings {
fontSize: number;
}
// Settings now requires both properties
const s: Settings = { theme: "dark", fontSize: 14 };
בקוד של אפליקציה זה רק לעתים רחוקות מה שרוצים, ומיזוג בטעות יכול לבלבל. השימוש האמיתי שלו הוא הוספת איברים לטיפוסים שאינם שלכם: אפשרויות של ספרייה, או משתנה גלובלי כמו Window. מתוך מודול, עטפו את ההצהרה ב-declare global:
declare global {
interface Window {
analytics: { track(event: string): void };
}
}
export {};
אחרי זה window.analytics.track("signup") עובר בדיקת טיפוסים בכל מקום בפרויקט. חבילות של הגדרות טיפוסים נשענות על אותו מנגנון; ראו declaration files.
ערכי ברירת מחדל למאפיינים של interface
interface לא יכול להחזיק ערכי ברירת מחדל, כי הוא מתאר טיפוסים ונמחק בזמן ריצה. size?: "sm" | "md" = "md" היא השגיאה TS1246, An interface property cannot have an initializer. הפכו את המאפיין לאופציונלי ומלאו את ברירת המחדל במקום שבו משתמשים באובייקט:
ערכי ברירת מחדל של destructuring הם הבחירה הבטוחה יותר: הם חלים בכל פעם שהערך הוא undefined, כולל size: undefined מפורש. גרסת ה-spread מעתיקה את ה-undefined המפורש מעל ברירת המחדל, והתוצאה שלה עדיין מקבלת טיפוס כאילו size תמיד מוגדר. אם אתם צריכים את ההבטחה הזו מהטיפוסים, הפעילו את exactOptionalPropertyTypes, שהופך את size: undefined לשגיאת קומפילציה עבור size?: ... אופציונלי. מחלקה עם שדות מאותחלים היא האפשרות השנייה כשהאובייקט צריך גם התנהגות.
interfaces גנריים
interface יכול לקבל פרמטרי טיפוס, וכך הצהרה אחת עובדת עם הרבה טיפוסים של תוכן:
interface ApiResponse<T> {
ok: boolean;
data: T;
error?: string;
}
interface Page<T> {
items: T[];
nextCursor?: string;
}
interface User {
id: number;
name: string;
}
const res: ApiResponse<Page<User>> = {
ok: true,
data: { items: [{ id: 1, name: "Ada" }], nextCursor: "abc" },
};
ApiResponse<Page<User>> נקרא כ"תשובה שהנתונים שלה הם עמוד של משתמשים". הספרייה הסטנדרטית מלאה באלה: Array<T>, Promise<T> ו-Map<K, V> הם כולם interfaces גנריים.
Interface מול type alias
type alias יכול לתאר את אותו מבנה אובייקט, ובטיפוסי אובייקט פשוטים אפשר להחליף בין השניים. רק interface יכול להתמזג; רק type alias יכול לתת שם ל-union, ל-tuple או ל-mapped type או conditional type. כלל האצבע של ה-handbook של TypeScript הוא להשתמש ב-interface עד שצריך תכונה שיש רק ל-type. בעמוד interface מול type יש את ההשוואה המלאה, כולל ההבדל של Record<string, ...> שמפתיע את רוב האנשים.
שאלות נפוצות
מה זה interface ב-TypeScript?
interface הוא תיאור עם שם של המבנה של אובייקט: שמות המאפיינים שלו, הטיפוסים שלהם, אילו מהם אופציונליים או readonly, והמתודות שלו. הקומפיילר בודק שלערכים שמשמשים כ-interface הזה יש את המבנה הזה. interfaces קיימים רק בזמן הקומפילציה; הם לא מייצרים JavaScript.
איך מגדירים ערך ברירת מחדל ב-interface של TypeScript?
אי אפשר: interface מתאר טיפוסים, לא ערכים, ולכן size: "md" = ... אינו תחביר תקין. סמנו את המאפיין כאופציונלי (size?: "sm" | "md") והחילו את ברירת המחדל במקום שבו משתמשים באובייקט, בדרך כלל עם ערכי ברירת מחדל של destructuring בפרמטרים של הפונקציה: function render({ size = "md" }: Options). גם spread של אובייקט ברירות מחדל ({ ...DEFAULTS, ...options }) עובד, אבל undefined מפורש ב-options דורס את ברירת המחדל.
איך בודקים בזמן ריצה האם אובייקט מממש interface?
אין דרך מובנית, כי interfaces נמחקים בזמן הקומפילציה: obj instanceof User היא השגיאה TS2693 ('User' only refers to a type, but is being used as a value here). כתבו פונקציית type guard שבודקת את המאפיינים, function isUser(x: unknown): x is User { ... }, או בצעו ולידציה עם ספריית סכמות.
האם interface יכול להרחיב כמה interfaces?
כן. כתבו אותם אחרי extends, מופרדים בפסיקים: interface ServiceDog extends Pet, Trained { ... }. ל-interface החדש יש כל איבר של כל הורה, ובנוסף האיברים שלו. אם שני הורים מצהירים על אותו מאפיין עם טיפוסים לא תואמים, ההצהרה היא שגיאה.
מה ההבדל בין interface למחלקה ב-TypeScript?
מחלקה קיימת בזמן ריצה: יש לה בנאי ומימושי מתודות, ו-new יוצר ממנה אובייקטים. interface רק מתאר מבנה בשביל הקומפיילר ונמחק מפלט ה-JavaScript. מחלקה יכולה להצהיר implements SomeInterface כדי שהקומפיילר יבדוק שהיא מתאימה, וגם כל אובייקט רגיל עם המבנה הנכון מתאים ל-interface.