לדלג לתוכן

5.6 גנריקס פתרון

פתרון - גנריקס

פתרון תרגיל 1

א. last:

function last<T>(arr: T[]): T | undefined {
    return arr[arr.length - 1];
}

console.log(last([1, 2, 3]));       // 3
console.log(last(["a", "b"]));      // "b"
console.log(last([]));               // undefined

ב. reverse:

function reverse<T>(arr: T[]): T[] {
    return [...arr].reverse();
}

console.log(reverse([1, 2, 3])); // [3, 2, 1]
let original = [1, 2, 3];
reverse(original);
console.log(original); // [1, 2, 3] - unchanged

ג. unique:

function unique<T>(arr: T[]): T[] {
    return [...new Set(arr)];
}

console.log(unique([1, 2, 2, 3, 3, 3])); // [1, 2, 3]
console.log(unique(["a", "b", "a"]));      // ["a", "b"]

ד. zip:

function zip<A, B>(arr1: A[], arr2: B[]): [A, B][] {
    let length = Math.min(arr1.length, arr2.length);
    let result: [A, B][] = [];
    for (let i = 0; i < length; i++) {
        result.push([arr1[i], arr2[i]]);
    }
    return result;
}

console.log(zip([1, 2, 3], ["a", "b", "c"])); // [[1, "a"], [2, "b"], [3, "c"]]
console.log(zip([1, 2], ["a", "b", "c"]));     // [[1, "a"], [2, "b"]]

פתרון תרגיל 2

א. KeyValuePair:

interface KeyValuePair<K, V> {
    key: K;
    value: V;
}

let pair1: KeyValuePair<string, number> = { key: "age", value: 30 };
let pair2: KeyValuePair<number, string> = { key: 1, value: "hello" };

ב. Repository:

interface Repository<T> {
    getAll(): T[];
    getById(id: number): T | undefined;
    create(item: T): T;
    update(id: number, item: Partial<T>): T | undefined;
    delete(id: number): boolean;
}

ג. InMemoryRepository:

class InMemoryRepository<T extends { id: number }> implements Repository<T> {
    private items: T[] = [];
    private nextId: number = 1;

    getAll(): T[] {
        return [...this.items];
    }

    getById(id: number): T | undefined {
        return this.items.find(item => item.id === id);
    }

    create(item: T): T {
        let newItem = { ...item, id: this.nextId++ };
        this.items.push(newItem);
        return newItem;
    }

    update(id: number, updates: Partial<T>): T | undefined {
        let index = this.items.findIndex(item => item.id === id);
        if (index === -1) return undefined;
        this.items[index] = { ...this.items[index], ...updates };
        return this.items[index];
    }

    delete(id: number): boolean {
        let index = this.items.findIndex(item => item.id === id);
        if (index === -1) return false;
        this.items.splice(index, 1);
        return true;
    }
}

// usage
interface User {
    id: number;
    name: string;
    email: string;
}

let userRepo = new InMemoryRepository<User>();
let alice = userRepo.create({ id: 0, name: "Alice", email: "alice@example.com" });
console.log(alice); // { id: 1, name: "Alice", email: "alice@example.com" }

let bob = userRepo.create({ id: 0, name: "Bob", email: "bob@example.com" });
console.log(userRepo.getAll()); // [alice, bob]

userRepo.update(1, { email: "new@example.com" });
console.log(userRepo.getById(1)); // { id: 1, name: "Alice", email: "new@example.com" }

פתרון תרגיל 3

א. merge:

function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
    return { ...obj1, ...obj2 };
}

let result = merge({ name: "Alice" }, { age: 30 });
console.log(result); // { name: "Alice", age: 30 }

ב. longest:

function longest<T extends { length: number }>(a: T, b: T): T {
    return a.length >= b.length ? a : b;
}

console.log(longest("hello", "hi"));       // "hello"
console.log(longest([1, 2, 3], [4, 5]));   // [1, 2, 3]

ג. groupBy:

function groupBy<T>(items: T[], key: keyof T): Record<string, T[]> {
    let result: Record<string, T[]> = {};
    for (let item of items) {
        let groupKey = String(item[key]);
        if (!result[groupKey]) {
            result[groupKey] = [];
        }
        result[groupKey].push(item);
    }
    return result;
}

let users = [
    { name: "Alice", role: "admin" },
    { name: "Bob", role: "user" },
    { name: "Charlie", role: "admin" }
];

let grouped = groupBy(users, "role");
console.log(grouped);
// { admin: [{ name: "Alice", role: "admin" }, { name: "Charlie", role: "admin" }],
//   user: [{ name: "Bob", role: "user" }] }

פתרון תרגיל 4

א. pluck:

function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
    return items.map(item => item[key]);
}

let users = [
    { name: "Alice", age: 30 },
    { name: "Bob", age: 25 }
];

let names = pluck(users, "name"); // string[] -> ["Alice", "Bob"]
let ages = pluck(users, "age");   // number[] -> [30, 25]

ב. setProperty:

function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): T {
    return { ...obj, [key]: value };
}

let user = { name: "Alice", age: 30 };
let updated = setProperty(user, "age", 31);     // ok
// setProperty(user, "age", "31");               // ERROR: string not assignable to number

ג. Optional:

type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface User {
    id: number;
    name: string;
    email: string;
    bio: string;
}

type CreateUserInput = Optional<User, "id" | "bio">;

let input: CreateUserInput = {
    name: "Alice",
    email: "alice@example.com"
    // id and bio are optional
};

הסבר: Omit<T, K> מסיר את השדות K מ-T. Partial<Pick<T, K>> לוקח רק את השדות K ומפך אותם לאופציונליים. ה-intersection (&) משלב את שניהם.

פתרון תרגיל 5

א. PaginatedResult:

interface PaginatedResult<T = unknown> {
    items: T[];
    total: number;
    page: number;
    pageSize: number;
    hasNextPage: boolean;
}

ב. paginate:

function paginate<T>(items: T[], page: number, pageSize: number): PaginatedResult<T> {
    let start = (page - 1) * pageSize;
    let end = start + pageSize;
    let paginatedItems = items.slice(start, end);

    return {
        items: paginatedItems,
        total: items.length,
        page,
        pageSize,
        hasNextPage: end < items.length
    };
}

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let page1 = paginate(numbers, 1, 3);
// { items: [1, 2, 3], total: 10, page: 1, pageSize: 3, hasNextPage: true }

let page4 = paginate(numbers, 4, 3);
// { items: [10], total: 10, page: 4, pageSize: 3, hasNextPage: false }

ג. mapPaginated:

function mapPaginated<T, U>(
    result: PaginatedResult<T>,
    fn: (item: T) => U
): PaginatedResult<U> {
    return {
        ...result,
        items: result.items.map(fn)
    };
}

let stringPage = mapPaginated(page1, (n) => `Item #${n}`);
// { items: ["Item #1", "Item #2", "Item #3"], total: 10, ... }

פתרון תרגיל 6

class BoundedStack<T> {
    private items: T[] = [];
    private maxSize: number;

    constructor(maxSize: number) {
        this.maxSize = maxSize;
    }

    push(item: T): boolean {
        if (this.isFull()) {
            return false;
        }
        this.items.push(item);
        return true;
    }

    pop(): T | undefined {
        return this.items.pop();
    }

    peek(): T | undefined {
        if (this.isEmpty()) return undefined;
        return this.items[this.items.length - 1];
    }

    isFull(): boolean {
        return this.items.length >= this.maxSize;
    }

    isEmpty(): boolean {
        return this.items.length === 0;
    }

    get size(): number {
        return this.items.length;
    }

    toArray(): T[] {
        return [...this.items];
    }
}

// usage with numbers
let numStack = new BoundedStack<number>(3);
console.log(numStack.push(1));  // true
console.log(numStack.push(2));  // true
console.log(numStack.push(3));  // true
console.log(numStack.push(4));  // false - full!
console.log(numStack.isFull()); // true
console.log(numStack.peek());   // 3
console.log(numStack.pop());    // 3
console.log(numStack.size);     // 2

// usage with strings
let strStack = new BoundedStack<string>(2);
strStack.push("hello");
strStack.push("world");
console.log(strStack.toArray()); // ["hello", "world"]

פתרון תרגיל 7

א. AppEvents:

interface AppEvents {
    userLogin: { userId: string; timestamp: number };
    userLogout: { userId: string };
    pageView: { path: string; referrer?: string };
    error: { message: string; stack?: string };
}

ב. EventBus:

class EventBus<T extends Record<string, unknown>> {
    private handlers: Map<keyof T, ((data: any) => void)[]> = new Map();

    on<K extends keyof T>(event: K, handler: (data: T[K]) => void): void {
        let existing = this.handlers.get(event) ?? [];
        existing.push(handler as (data: any) => void);
        this.handlers.set(event, existing);
    }

    off<K extends keyof T>(event: K, handler: (data: T[K]) => void): void {
        let existing = this.handlers.get(event);
        if (!existing) return;
        let index = existing.indexOf(handler as (data: any) => void);
        if (index !== -1) {
            existing.splice(index, 1);
        }
    }

    emit<K extends keyof T>(event: K, data: T[K]): void {
        let existing = this.handlers.get(event);
        if (!existing) return;
        for (let handler of existing) {
            handler(data);
        }
    }

    once<K extends keyof T>(event: K, handler: (data: T[K]) => void): void {
        let wrapper = (data: T[K]) => {
            handler(data);
            this.off(event, wrapper);
        };
        this.on(event, wrapper);
    }
}

ג. דוגמת שימוש:

let bus = new EventBus<AppEvents>();

// register handlers
bus.on("userLogin", (data) => {
    // TS knows: data is { userId: string; timestamp: number }
    console.log(`User ${data.userId} logged in at ${data.timestamp}`);
});

bus.on("error", (data) => {
    // TS knows: data is { message: string; stack?: string }
    console.log(`Error: ${data.message}`);
});

bus.once("pageView", (data) => {
    console.log(`First page view: ${data.path}`);
});

// emit events - type safe!
bus.emit("userLogin", { userId: "123", timestamp: Date.now() }); // ok
bus.emit("pageView", { path: "/home" });                          // ok

// these would cause compile errors:
// bus.emit("userLogin", { userId: 123 });        // ERROR: number not assignable to string
// bus.emit("unknownEvent", {});                   // ERROR: not a key of AppEvents
// bus.emit("error", { msg: "oops" });             // ERROR: 'msg' doesn't exist, 'message' required