לתיאור מבנה של אובייקט, interface ו-type עושים את אותה עבודה, וערכים של אחד ניתנים להשמה לשני כשהמבנים מתאימים. ההבדלים נמצאים בשוליים: type יכול לתת שם לדברים ש-interface לא יכול (unions, tuples, טיפוסים מחושבים), ו-interface יכול לעשות כמה דברים ש-type alias לא יכול (מיזוג, extends שנבדק).
TypeScript משווה טיפוסי אובייקט לפי מבנה, ולכן לשם ההצהרה אין משמעות לגבי השמה. מה שבא בהמשך הוא רשימת המקומות שבהם הבחירה כן משנה.
טבלת השוואה
| תכונה | interface | type |
|---|---|---|
| מבנים של אובייקטים | כן | כן |
| מאפיינים אופציונליים, readonly, מתודות, index signatures | כן | כן |
| Generics | כן | כן |
Unions (A | B) | לא | כן |
| Tuples, טיפוסים פרימיטיביים, טיפוסי פונקציות כשלעצמם | לא (טיפוסי פונקציות רק כחתימות קריאה) | כן |
| Mapped types ו-conditional types | לא | כן |
| הרחבה | extends, התנגשויות הן שגיאות | &, התנגשויות הופכות ל-never |
| Declaration merging | כן | לא (duplicate identifier) |
ניתן להשמה ל-Record<string, T> | לא | כן, כשהמאפיינים מתאימים |
implements במחלקה | כן | כן, אם זה טיפוס אובייקט |
| הגדרות רקורסיביות | כן | כן |
מה רק type יכול לעשות
כל דבר שאינו מבנה אובייקט יחיד צריך type alias:
אף אחד מאלה לא ניתן לכתיבה עם interface (שני האחרונים מוסברים בעמודים על mapped types ועל conditional types). זו הסיבה המעשית שכל בסיס קוד משתמש בסוף ב-type איפשהו, לא משנה במה הוא משתמש למבנים של אובייקטים.
מה רק interface יכול לעשות: declaration merging
שתי הצהרות interface עם אותו שם באותו scope מתאחדות לאחת. שתי הצהרות type עם אותו שם הן השגיאה TS2300, Duplicate identifier.
interface Settings {
theme: string;
}
interface Settings {
fontSize: number;
}
const s: Settings = { theme: "dark", fontSize: 14 }; // needs both
type Options = { a: number };
type Options = { b: number }; // error TS2300: Duplicate identifier 'Options'.
מיזוג הוא הדרך שבה מרחיבים טיפוסים של ספריות מבחוץ: הוספת מאפיין ל-Window הגלובלי, ל-Request של Express, או לטיפוס ה-theme של ספרייה. אם אתם מפרסמים טיפוסים שמשתמשים עשויים להצטרך להרחיב, השתמשו ב-interfaces. בקוד של האפליקציה שלכם, מיזוג בטעות (שני קובצי script שמצהירים על אותו שם של interface גלובלי) קורה בשקט, אלא אם שניהם מצהירים על אותו מאפיין עם טיפוסים שונים, וזה אחד הנימוקים שחלק מהצוותים נותנים להעדפת type.
extends מול intersection
interface מתרחב עם extends, ו-type alias עם &. ברוב המקרים הם מייצרים את אותה תוצאה, אבל הם מטפלים אחרת במאפיין מתנגש. extends מדווח על ההתנגשות כבר בהצהרה:
index.ts(7,11): error TS2430: Interface 'Broken' incorrectly extends interface 'Base'.
Types of property 'id' are incompatible.
Type 'number' is not assignable to type 'string'.
intersection מקבל את אותה התנגשות בשקט והופך את המאפיין ל-never (string שהוא גם number). השגיאה מופיעה רק מאוחר יותר, כשמנסים ליצור ערך:
השגיאה מצביעה על האובייקט, רחוק מההצהרה שגרמה לה. לבניית טיפוסי אובייקט מתוך טיפוסי אובייקט אחרים, extends נותן הודעה טובה יותר.
Index signatures: הבדל שקל לפספס
type alias של טיפוס אובייקט מקבל index signature מרומזת, ולכן אפשר להעביר אותו במקום שבו מצופה Record<string, unknown>. interface לא מקבל. זה בכוונה: ההסבר של צוות TypeScript (issue #15300 במאגר של TypeScript) הוא שאפשר להרחיב interface בהצהרות מאוחרות יותר, ולכן הסקה של index signature עבורו פחות בטוחה, ושינוי הכלל עכשיו היה שובר יותר מדי קוד.
הקריאה שמסומנת ב-@ts-expect-error עדיין רצה ומדפיסה את השדות של Alan: הכלל הוא כלל של זמן קומפילציה. זו הסיבה הרגילה לשגיאה מבלבלת כשמעבירים ערך של interface לפונקציית עזר ללוגים, לסריאליזציה או לשאילתות שהטיפוס שלה הוא Record<string, ...>. החליפו את ההצהרה הזו ל-type, עשו spread לערך, או תנו לפרמטר של פונקציית העזר טיפוס של interface או גנרי.
ביצועים והודעות שגיאה
עמוד הביצועים בוויקי של TypeScript (הסעיף "Preferring Interfaces Over Intersections") ממליץ על interface Foo extends Bar, Baz { ... } במקום type Foo = Bar & Baz & { ... } בהרכבה של טיפוסי אובייקט. הנימוקים שלו: interface הוא טיפוס אובייקט שטוח יחיד שמזהה התנגשויות בין מאפיינים, יחסי טיפוסים בין interfaces נשמרים במטמון (intersections בשלמותם לא), ובדיקה של ערך מול intersection בודקת כל רכיב לפני הטיפוס המשוטח. ההבדל חשוב בבסיסי קוד גדולים עם הרבה טיפוסים מורכבים; בכמה מבנים רגילים של אובייקטים לא תוכלו למדוד אותו.
אותו עמוד מציין ש-interfaces מוצגים טוב יותר. interface מוצג לפי השם שלו בחלונות ריחוף ובהודעות שגיאה, ואילו alias של intersection מודפס לעתים קרובות כשהוא פרוס לחלקיו, מה שמקשה על קריאת הודעות ארוכות.
במה להשתמש
כלל שעובד:
- מבנים של אובייקטים:
interface. הוא נותןextendsשנבדק, שגיאות ברורות יותר בהרכבות גדולות, ומאפשר למשתמשי ספריות להרחיב אותו. זה תואם לכלל האצבע של ה-handbook של TypeScript: "השתמשו ב-interfaceעד שתצטרכו תכונות שלtype". - כל השאר:
type. Unions, tuples, טיפוסי פונקציות, טיפוסי ליטרל, וכל מה שנבנה עם mapped types, conditional types או template literal types. - חריג: השתמשו ב-
typeלמבנה אובייקט שצריך להתאים לפרמטרים שלRecord<string, ...>, או כשאתם רוצים בכוונה למנוע מיזוג.
שימוש ב-type לכל דבר הוא גם בחירה עקבית, והרבה בסיסי קוד עושים את זה. ערבוב של שניהם באקראי הוא האפשרות היחידה שכדאי להימנע ממנה: הוא גורם לקוראים לתהות האם ההבדל היה מכוון.
שאלות נפוצות
מה ההבדל בין type ל-interface ב-TypeScript?
שניהם מתארים מבנים של אובייקטים, ולצורך הזה אפשר להחליף ביניהם. type יכול גם לתת שם ל-unions, ל-tuples, לטיפוסים פרימיטיביים ול-mapped types או conditional types, ו-interface לא יכול. interface תומך ב-declaration merging וב-extends, שבודק מאפיינים מתנגשים. טיפוסי אובייקט שנכתבו עם type גם ניתנים להשמה לטיפוסים עם index signature כמו Record<string, unknown>, ו-interfaces לא.
כדאי להשתמש ב-type או ב-interface?
כלל האצבע של ה-handbook של TypeScript הוא: השתמשו ב-interface עד שתצטרכו תכונה שיש רק ל-type. בפועל זה אומר interfaces למבנים של אובייקטים ו-type ל-unions, ל-tuples, לטיפוסי פונקציות ולטיפוסים מחושבים. גם צוותים שמשתמשים ב-type לכל דבר מסתדרים מצוין; מה שחשוב הוא כלל אחד ועקבי.
האם interface מהיר יותר מ-type ב-TypeScript?
בהרכבה של טיפוסי אובייקט, כן, בחלק מהמקרים. הנחיות הביצועים של צוות TypeScript ממליצות על interface ... extends במקום intersections גדולים (A & B & { ... }), כי יחסים בין interfaces נשמרים במטמון ו-interface הוא טיפוס שטוח אחד. למבנה אובייקט פשוט אין הבדל משמעותי.
האם מחלקה יכולה לממש type alias?
כן, אם ה-alias הוא טיפוס אובייקט (או intersection של טיפוסי אובייקט): class Point implements PointType { ... } עובד. מחלקה לא יכולה לממש טיפוס union; זו השגיאה TS2422.
האם interface יכול להרחיב type alias?
כן, כל עוד ה-alias הוא טיפוס אובייקט: type Base = { id: string }; interface User extends Base { name: string } תקין. הוא לא יכול להרחיב alias של union. בכיוון ההפוך, type alias יכול להיבנות על interface עם &.