לדלג לתוכן

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 merging
  • type תומך ב-unions (|) ו-intersections (&)
  • שדה אופציונלי עם ? יכול להיות undefined
  • readonly מונע שינוי של שדות
  • index signatures מאפשרים מפתחות דינמיים
  • הרחבה עם extends או & מאפשרת לבנות טיפוסים מורכבים מטיפוסים פשוטים
  • טיפוסים מקוננים עדיף לפרק לאינטרפייסים נפרדים