5.3 אינטרפייסים וטייפים הרצאה
אינטרפייסים וטייפים - interfaces and types¶
בשיעור הקודם ראינו שאפשר לתאר את צורת האובייקט ישירות (inline), אבל זה הפך למסורבל כשהטיפוס חוזר על עצמו. הפתרון: interface ו-type alias - שתי דרכים לתת שם לטיפוס ולהשתמש בו שוב ושוב.
אינטרפייס - interface¶
אינטרפייס מגדיר את הצורה (shape) של אובייקט - אילו שדות יש לו ומה הטיפוס של כל שדה:
interface User {
name: string;
age: number;
email: string;
}
let user: User = {
name: "Alice",
age: 30,
email: "alice@example.com"
};
עכשיו אפשר להשתמש ב-User בכל מקום - בפרמטרים של פונקציות, בטיפוסי החזרה, במערכים:
function greetUser(user: User): string {
return `Hello, ${user.name}! You are ${user.age} years old.`;
}
let users: User[] = [
{ name: "Alice", age: 30, email: "alice@example.com" },
{ name: "Bob", age: 25, email: "bob@example.com" }
];
כינוי טיפוס - type alias¶
type עושה דבר דומה - נותן שם לטיפוס:
type User = {
name: string;
age: number;
email: string;
};
let user: User = {
name: "Alice",
age: 30,
email: "alice@example.com"
};
שימו לב: type משתמש בסימן =, ו-interface לא.
ההבדלים בין interface ל-type¶
שניהם יכולים לתאר אובייקטים, אבל יש הבדלים:
מה ש-type יכול ו-interface לא¶
type יכול לתאר כל טיפוס, לא רק אובייקטים:
// union type - only with type
type Status = "active" | "inactive" | "pending";
// tuple - only with type
type Coordinate = [number, number];
// primitive alias - only with type
type ID = string | number;
מה ש-interface יכול ו-type לא¶
אינטרפייס תומך ב-declaration merging - הגדרה כפולה מתמזגת:
interface User {
name: string;
}
interface User {
age: number;
}
// User is now { name: string; age: number }
זה שימושי בעיקר לספריות שרוצות לאפשר הרחבה. בקוד רגיל זה פחות רלוונטי.
מתי להשתמש במה¶
הכלל המקובל:
- interface - כשמגדירים צורה של אובייקט (הרוב המוחלט של המקרים)
- type - כשצריך unions, tuples, או טיפוסים שהם לא אובייקטים
בפרקטיקה, הרבה צוותים בוחרים אחד ומשתמשים בו בעקביות. שניהם עובדים מצוין.
שדות אופציונליים - optional properties¶
סימן שאלה ? אחרי שם השדה הופך אותו לאופציונלי:
interface Product {
name: string;
price: number;
description?: string; // optional - can be string or undefined
discount?: number; // optional
}
// both are valid:
let product1: Product = { name: "Laptop", price: 999 };
let product2: Product = { name: "Phone", price: 699, description: "Latest model", discount: 10 };
שדה אופציונלי הוא בעצם type | undefined. כשניגשים אליו, צריך לטפל באפשרות שהוא undefined:
function getDescription(product: Product): string {
// product.description is string | undefined
return product.description ?? "No description available";
}
קריאה בלבד - readonly¶
readonly מונע שינוי של שדה אחרי האתחול:
interface Point {
readonly x: number;
readonly y: number;
}
let point: Point = { x: 10, y: 20 };
point.x = 5; // ERROR: Cannot assign to 'x' because it is a read-only property
שימו לב: readonly הוא שטחי (shallow). אם השדה הוא אובייקט, אפשר לשנות את ה-properties הפנימיים שלו:
interface Config {
readonly settings: {
theme: string;
language: string;
};
}
let config: Config = { settings: { theme: "dark", language: "he" } };
config.settings = { theme: "light", language: "en" }; // ERROR - can't reassign
config.settings.theme = "light"; // OK! - readonly is shallow
כדי ש-readonly יהיה עמוק, צריך להוסיף readonly גם לאובייקט הפנימי, או להשתמש ב-Readonly<T> (נלמד בשיעור 5.7).
חתימת אינדקס - index signatures¶
כשלא יודעים מראש אילו מפתחות יהיו באובייקט, משתמשים ב-index signature:
interface Dictionary {
[key: string]: string;
}
let translations: Dictionary = {
hello: "שלום",
goodbye: "להתראות",
thanks: "תודה"
};
translations["yes"] = "כן"; // ok - any string key, string value
translations["no"] = 42; // ERROR: Type 'number' is not assignable to type 'string'
אפשר לשלב שדות מוגדרים עם index signature:
interface UserMap {
[id: string]: { name: string; age: number };
admin: { name: string; age: number }; // specific key, must match the index type
}
אפשר גם להשתמש ב-number כמפתח (מתאים למערכים):
interface NumberMap {
[index: number]: string;
}
let arr: NumberMap = ["hello", "world"];
console.log(arr[0]); // "hello"
הרחבה - extending interfaces¶
אינטרפייס יכול לרשת שדות מאינטרפייס אחר:
interface Animal {
name: string;
age: number;
}
interface Dog extends Animal {
breed: string;
bark(): void;
}
// Dog has: name, age, breed, bark
let dog: Dog = {
name: "Rex",
age: 5,
breed: "German Shepherd",
bark() {
console.log("Woof!");
}
};
אפשר לרשת ממספר אינטרפייסים:
interface Printable {
print(): void;
}
interface Loggable {
log(message: string): void;
}
interface Report extends Printable, Loggable {
title: string;
data: unknown[];
}
חיתוך - intersection types¶
עם type, אפשר לשלב טיפוסים עם & (intersection):
type Animal = {
name: string;
age: number;
};
type Pet = {
owner: string;
};
type PetAnimal = Animal & Pet;
// PetAnimal has: name, age, owner
let myPet: PetAnimal = {
name: "Buddy",
age: 3,
owner: "Alice"
};
intersection דורש שכל השדות מכל הטיפוסים יהיו קיימים. זה דומה ל-extends, אבל עובד עם type.
extends לעומת intersection¶
// with interface - extends
interface A { x: number; }
interface B extends A { y: number; }
// with type - intersection
type A2 = { x: number };
type B2 = A2 & { y: number };
// both result in { x: number; y: number }
ההבדל המעשי: extends נותן שגיאה ברורה יותר כשיש קונפליקט בין הטיפוסים, בזמן ש-& ישקט ויצור טיפוס never לשדה הבעייתי.
טיפוסים מקוננים - nested types¶
אינטרפייסים יכולים להכיל אובייקטים מקוננים:
interface Address {
street: string;
city: string;
zipCode: string;
}
interface Company {
name: string;
address: Address;
employees: Employee[];
}
interface Employee {
name: string;
role: string;
address?: Address; // reuse the same Address interface
}
הפרדה לאינטרפייסים קטנים היא practice טוב - קל יותר לקרוא, לתחזק, ולהשתמש מחדש.
דוגמה מעשית - תגובה מ-API¶
interface ApiResponse<T> {
status: number;
message: string;
data: T;
timestamp: string;
}
interface UserProfile {
id: number;
username: string;
email: string;
preferences: {
theme: "light" | "dark";
language: string;
notifications: boolean;
};
}
// usage
let response: ApiResponse<UserProfile> = {
status: 200,
message: "Success",
data: {
id: 1,
username: "alice",
email: "alice@example.com",
preferences: {
theme: "dark",
language: "he",
notifications: true
}
},
timestamp: "2025-01-01T00:00:00Z"
};
שימו לב: השתמשנו כאן ב-generics (<T>) - נלמד עליהם לעומק בשיעור 5.6.
פונקציות באינטרפייס¶
אפשר להגדיר מתודות (פונקציות) בתוך אינטרפייס:
interface Calculator {
add(a: number, b: number): number;
subtract(a: number, b: number): number;
reset(): void;
}
// or with arrow syntax
interface Calculator2 {
add: (a: number, b: number) => number;
subtract: (a: number, b: number) => number;
reset: () => void;
}
שני התחבירים עובדים. הראשון (method syntax) נפוץ יותר באינטרפייסים.
type alias לפונקציות¶
עם type אפשר להגדיר את הטיפוס של פונקציה:
type MathOperation = (a: number, b: number) => number;
let add: MathOperation = (a, b) => a + b;
let multiply: MathOperation = (a, b) => a * b;
function applyOperation(x: number, y: number, op: MathOperation): number {
return op(x, y);
}
applyOperation(5, 3, add); // 8
applyOperation(5, 3, multiply); // 15
סיכום¶
interfaceמגדיר צורה של אובייקט - שדות, טיפוסים, ומתודותtype aliasנותן שם לכל טיפוס - אובייקטים, unions, tuples, ועודinterfaceתומך ב-extends וב-declaration mergingtypeתומך ב-unions (|) ו-intersections (&)- שדה אופציונלי עם
?יכול להיות undefined readonlyמונע שינוי של שדות- index signatures מאפשרים מפתחות דינמיים
- הרחבה עם
extendsאו&מאפשרת לבנות טיפוסים מורכבים מטיפוסים פשוטים - טיפוסים מקוננים עדיף לפרק לאינטרפייסים נפרדים