לדלג לתוכן

5.8 פרויקטים פרויקט

פרויקטים - טייפסקריפט

פרויקט 1 - אפליקציית Todo בטייפסקריפט

תיאור

המירו אפליקציית Todo מג׳אווהסקריפט לטייפסקריפט. המטרה: להוסיף טיפוסים מלאים לכל חלק באפליקציה - מודלים, פונקציות, וניהול state.

דרישות

מודל הנתונים

הגדירו את הטיפוסים הבאים:

  • Priority - union literal: "low" | "medium" | "high"
  • TodoStatus - union literal: "active" | "completed" | "archived"
  • Todo - אינטרפייס עם:
  • id: string (readonly)
  • title: string
  • description?: string
  • priority: Priority
  • status: TodoStatus
  • tags: string[]
  • createdAt: Date (readonly)
  • updatedAt: Date
  • completedAt?: Date
  • CreateTodoInput - מבוסס על Omit<Todo, "id" | "createdAt" | "updatedAt" | "completedAt" | "status"> (השדות האלה נוצרים אוטומטית)
  • UpdateTodoInput - מבוסס על Partial<Pick<Todo, "title" | "description" | "priority" | "tags">>
  • TodoFilter - אינטרפייס עם שדות אופציונליים: status, priority, searchTerm, tags
פונקציות CRUD

ממשו את הפונקציות הבאות עם טיפוסים מלאים:

function createTodo(input: CreateTodoInput): Todo
function updateTodo(todo: Todo, updates: UpdateTodoInput): Todo
function deleteTodo(todos: Todo[], id: string): Todo[]
function toggleTodo(todo: Todo): Todo  // switches between active/completed
function archiveTodo(todo: Todo): Todo
סינון ומיון
function filterTodos(todos: Todo[], filter: TodoFilter): Todo[]
function sortTodos(todos: Todo[], sortBy: "title" | "priority" | "createdAt" | "status"): Todo[]
function searchTodos(todos: Todo[], query: string): Todo[]
סטטיסטיקות
interface TodoStats {
    total: number;
    active: number;
    completed: number;
    archived: number;
    byPriority: Record<Priority, number>;
    completionRate: number; // percentage
}

function getTodoStats(todos: Todo[]): TodoStats
אחסון
function saveTodos(todos: Todo[]): void        // save to localStorage
function loadTodos(): Todo[]                    // load from localStorage

שימו לב: כשטוענים מ-localStorage, צריך להמיר מחרוזות בחזרה ל-Date. הוסיפו פונקציית עזר עם טיפוסים נכונים.

דגשים

  • אל תשתמשו ב-any בשום מקום
  • כל פונקציה חייבת להיות מוטייפת לחלוטין
  • השתמשו ב-discriminated unions, utility types, ו-generics כשמתאים
  • צרו קובץ types.ts נפרד לכל הטיפוסים

פרויקט 2 - ספריית utility מוטייפת

תיאור

בנו ספריית utility functions בטייפסקריפט עם דגש על גנריקס וטיפוסים מדויקים. כל פונקציה צריכה להיות גנרית ובטוחה מבחינת טיפוסים.

דרישות

מודול מערכים - arrays.ts
// returns the first element, or undefined for empty arrays
function first<T>(arr: T[]): T | undefined

// returns the last element, or undefined for empty arrays
function last<T>(arr: T[]): T | undefined

// splits array into chunks of given size
function chunk<T>(arr: T[], size: number): T[][]

// removes duplicates (by reference or by key function)
function unique<T>(arr: T[]): T[]
function uniqueBy<T, K>(arr: T[], keyFn: (item: T) => K): T[]

// groups items by a key extracted from each item
function groupBy<T>(arr: T[], keyFn: (item: T) => string): Record<string, T[]>

// creates a lookup map from an array
function keyBy<T>(arr: T[], keyFn: (item: T) => string): Record<string, T>

// returns intersection of two arrays
function intersection<T>(arr1: T[], arr2: T[]): T[]

// returns difference (items in arr1 but not in arr2)
function difference<T>(arr1: T[], arr2: T[]): T[]

// flattens nested arrays one level deep
function flatten<T>(arr: (T | T[])[]): T[]

// zips two arrays into array of pairs
function zip<A, B>(arr1: A[], arr2: B[]): [A, B][]

// sort by a key (returns new array)
function sortBy<T>(arr: T[], keyFn: (item: T) => number | string): T[]
מודול אובייקטים - objects.ts
// picks specified keys from an object
function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>

// omits specified keys from an object
function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K>

// deep clones an object (handle Date, Array, nested objects)
function deepClone<T>(obj: T): T

// deep merges two objects
function deepMerge<T extends object, U extends object>(target: T, source: U): T & U

// maps over object values, preserving keys
function mapValues<T extends object, U>(
    obj: T,
    fn: (value: T[keyof T], key: keyof T) => U
): Record<keyof T, U>

// filters object entries by predicate
function filterEntries<T extends object>(
    obj: T,
    predicate: (key: keyof T, value: T[keyof T]) => boolean
): Partial<T>
מודול פונקציות - functions.ts
// memoizes a function (cache results by arguments)
function memoize<T extends (...args: any[]) => any>(fn: T): T

// debounce - delays execution until quiet period
function debounce<T extends (...args: any[]) => void>(fn: T, delayMs: number): T

// throttle - limits execution to once per interval
function throttle<T extends (...args: any[]) => void>(fn: T, intervalMs: number): T

// pipe - chains functions left to right
function pipe<T>(value: T, ...fns: ((value: T) => T)[]): T

// retry - retries an async function on failure
function retry<T>(fn: () => Promise<T>, maxRetries: number, delayMs?: number): Promise<T>
מודול Result - result.ts

ממשו את דפוס ה-Result עם פונקציות עזר:

type Result<T, E = string> =
    | { ok: true; value: T }
    | { ok: false; error: E };

function ok<T>(value: T): Result<T, never>
function err<E>(error: E): Result<never, E>
function isOk<T, E>(result: Result<T, E>): result is { ok: true; value: T }
function isErr<T, E>(result: Result<T, E>): result is { ok: false; error: E }
function map<T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E>
function flatMap<T, U, E>(result: Result<T, E>, fn: (value: T) => Result<U, E>): Result<U, E>
function unwrapOr<T, E>(result: Result<T, E>, defaultValue: T): T
function tryCatch<T>(fn: () => T): Result<T, Error>

דגשים

  • כתבו טיפוסים מדויקים ככל האפשר - אם פונקציה מקבלת מערך של number והיא צריכה להחזיר number, הטיפוס צריך לשקף את זה
  • אל תשתמשו ב-any (חוץ מאילוצים בחתימות גנריות כמו (...args: any[]) => any)
  • כל פונקציה צריכה לעבוד עם כל טיפוס (גנריקס)
  • ארגנו את הקוד בקבצים נפרדים עם exports

פרויקט 3 - API client מוטייפ

תיאור

בנו wrapper מוטייפ סביב fetch שמספק type safety מלא - הטיפוסים של ה-request ושל ה-response נקבעים לפי ה-endpoint.

דרישות

הגדרת ה-API

הגדירו את מבנה ה-API כטיפוסים:

// each endpoint defines its methods and their request/response types
interface ApiSchema {
    "/users": {
        GET: {
            query: { page?: number; limit?: number; search?: string };
            response: { users: User[]; total: number };
        };
        POST: {
            body: CreateUserInput;
            response: User;
        };
    };
    "/users/:id": {
        GET: {
            params: { id: string };
            response: User;
        };
        PUT: {
            params: { id: string };
            body: UpdateUserInput;
            response: User;
        };
        DELETE: {
            params: { id: string };
            response: { success: boolean };
        };
    };
    "/posts": {
        GET: {
            query: { userId?: string; page?: number };
            response: { posts: Post[]; total: number };
        };
        POST: {
            body: CreatePostInput;
            response: Post;
        };
    };
}

הגדירו את הטיפוסים User, Post, CreateUserInput, UpdateUserInput, CreatePostInput.

ה-API client

בנו class ApiClient שמספק מתודות type-safe:

class ApiClient<Schema extends Record<string, any>> {
    constructor(private baseUrl: string, private defaultHeaders?: Record<string, string>)

    // GET request - returns the response type defined in the schema
    get<Path extends keyof Schema>(
        path: Path,
        options?: { query?: ...; params?: ...; headers?: ... }
    ): Promise<ApiResult<ResponseType>>

    // POST request
    post<Path extends keyof Schema>(
        path: Path,
        body: BodyType,
        options?: { params?: ...; headers?: ... }
    ): Promise<ApiResult<ResponseType>>

    // PUT request
    put<Path extends keyof Schema>(
        path: Path,
        body: BodyType,
        options?: { params?: ...; headers?: ... }
    ): Promise<ApiResult<ResponseType>>

    // DELETE request
    delete<Path extends keyof Schema>(
        path: Path,
        options?: { params?: ...; headers?: ... }
    ): Promise<ApiResult<ResponseType>>
}
טיפוסי תוצאה
type ApiResult<T> =
    | { ok: true; data: T; status: number }
    | { ok: false; error: ApiError; status: number };

interface ApiError {
    code: string;
    message: string;
    details?: unknown;
}
Interceptors

הוסיפו תמיכה ב-interceptors - פונקציות שרצות לפני ואחרי כל request:

interface RequestInterceptor {
    (config: RequestConfig): RequestConfig | Promise<RequestConfig>;
}

interface ResponseInterceptor {
    (response: Response): Response | Promise<Response>;
}

// add to ApiClient:
addRequestInterceptor(interceptor: RequestInterceptor): void
addResponseInterceptor(interceptor: ResponseInterceptor): void

שימוש לדוגמה - הוספת token:

client.addRequestInterceptor((config) => {
    let token = localStorage.getItem("token");
    if (token) {
        config.headers["Authorization"] = `Bearer ${token}`;
    }
    return config;
});
דוגמת שימוש
let client = new ApiClient<ApiSchema>("https://api.example.com");

// GET /users - TS knows the response type
let usersResult = await client.get("/users", { query: { page: 1, limit: 10 } });
if (usersResult.ok) {
    let users = usersResult.data.users; // User[]
    let total = usersResult.data.total; // number
}

// POST /users - TS enforces the body type
let createResult = await client.post("/users", {
    name: "Alice",
    email: "alice@example.com"
});

// type errors caught at compile time:
// client.get("/nonexistent");               // ERROR: not a valid path
// client.post("/users", { invalid: true }); // ERROR: doesn't match CreateUserInput

דגשים

  • הטיפוסים צריכים להיות מחמירים - אם ה-schema לא מגדיר POST עבור endpoint מסוים, הקומפיילר צריך לתת שגיאה
  • טפלו ב-URL params - החליפו :id בערך האמיתי
  • טפלו ב-query string - הוסיפו query params ל-URL
  • טפלו בשגיאות רשת, timeout, ותגובות לא תקינות
  • השתמשו בדפוס Result במקום לזרוק exceptions
  • כתבו דוגמאות שימוש שמדגימות את ה-type safety