לדלג לתוכן

5.4 פונקציות וצמצום טיפוסים פתרון

פתרון - פונקציות וצמצום טיפוסים

פתרון תרגיל 1

א. clamp:

function clamp(value: number, min: number, max: number): number {
    if (value < min) return min;
    if (value > max) return max;
    return value;
}

console.log(clamp(5, 1, 10));   // 5
console.log(clamp(-3, 1, 10));  // 1
console.log(clamp(15, 1, 10));  // 10

ב. repeatString:

function repeatString(str: string, times: number = 2): string {
    return str.repeat(times);
}

console.log(repeatString("ha"));     // "haha"
console.log(repeatString("ha", 3));  // "hahaha"

ג. joinStrings:

function joinStrings(separator: string, ...strings: string[]): string {
    return strings.join(separator);
}

console.log(joinStrings(", ", "Alice", "Bob", "Charlie")); // "Alice, Bob, Charlie"
console.log(joinStrings(" - ", "a", "b"));                 // "a - b"

פתרון תרגיל 2

א-ב. MathOperation ופונקציות:

type MathOperation = (a: number, b: number) => number;

let add: MathOperation = (a, b) => a + b;
let subtract: MathOperation = (a, b) => a - b;
let multiply: MathOperation = (a, b) => a * b;
let divide: MathOperation = (a, b) => a / b;

ג. calculate:

function calculate(a: number, b: number, operation: MathOperation): number | null {
    if (operation === divide && b === 0) {
        return null;
    }
    return operation(a, b);
}

console.log(calculate(10, 3, add));      // 13
console.log(calculate(10, 3, divide));   // 3.333...
console.log(calculate(10, 0, divide));   // null

ד. applyAll:

function applyAll(initial: number, operations: MathOperation[]): number {
    let result = initial;
    for (let op of operations) {
        result = op(result, 0);
    }
    return result;
}

// example: start with 5, add 0, multiply by 0 = 0
console.log(applyAll(5, [add, multiply])); // 0

הערה: כשהפרמטר השני תמיד 0, התוצאות לא מאוד מעניינות. הרעיון בתרגיל הוא להבין את הטיפוסים.

פתרון תרגיל 3

function stringify(value: string | number | boolean | null | undefined): string {
    if (value === null) {
        return "null";
    }
    if (value === undefined) {
        return "undefined";
    }
    if (typeof value === "string") {
        return `"${value}"`;
    }
    if (typeof value === "number") {
        return value.toString();
    }
    // value is boolean here
    return value.toString();
}

console.log(stringify("hello"));    // '"hello"'
console.log(stringify(3.14));       // "3.14"
console.log(stringify(true));       // "true"
console.log(stringify(null));       // "null"
console.log(stringify(undefined));  // "undefined"

שימו לב: null ו-undefined נבדקים עם === ולא עם typeof, כי typeof null מחזיר "object" (באג היסטורי בג׳אווהסקריפט).

פתרון תרגיל 4

א. handleError:

class ValidationError {
    constructor(public field: string, public message: string) {}
}

class NetworkError {
    constructor(public url: string, public statusCode: number) {}
}

class TimeoutError {
    constructor(public url: string, public timeoutMs: number) {}
}

function handleError(error: ValidationError | NetworkError | TimeoutError): string {
    if (error instanceof ValidationError) {
        return `Validation failed for field '${error.field}': ${error.message}`;
    }
    if (error instanceof NetworkError) {
        return `Network error (${error.statusCode}) fetching ${error.url}`;
    }
    // error is TimeoutError
    return `Request to ${error.url} timed out after ${error.timeoutMs}ms`;
}

console.log(handleError(new ValidationError("email", "Invalid format")));
// "Validation failed for field 'email': Invalid format"

console.log(handleError(new NetworkError("https://api.example.com", 404)));
// "Network error (404) fetching https://api.example.com"

console.log(handleError(new TimeoutError("https://api.example.com", 5000)));
// "Request to https://api.example.com timed out after 5000ms"

ב. isRetryable:

function isRetryable(error: ValidationError | NetworkError | TimeoutError): boolean {
    if (error instanceof NetworkError) {
        return error.statusCode >= 500;
    }
    if (error instanceof TimeoutError) {
        return true;
    }
    return false; // ValidationError is not retryable
}

פתרון תרגיל 5

א. טיפוסים:

interface SuccessResponse {
    status: "success";
    data: unknown;
    message?: string;
}

interface ErrorResponse {
    status: "error";
    errorCode: number;
    errorMessage: string;
}

interface LoadingResponse {
    status: "loading";
    progress?: number;
}

interface IdleResponse {
    status: "idle";
}

type ApiResponse = SuccessResponse | ErrorResponse | LoadingResponse | IdleResponse;

ב-ג. renderResponse עם exhaustive checking:

function renderResponse(response: ApiResponse): string {
    switch (response.status) {
        case "success":
            return `<div class='success'>Data: ${JSON.stringify(response.data)}</div>`;
        case "error":
            return `<div class='error'>Error ${response.errorCode}: ${response.errorMessage}</div>`;
        case "loading":
            if (response.progress !== undefined) {
                return `<div class='loading'>Loading... ${response.progress}%</div>`;
            }
            return `<div class='loading'>Loading...</div>`;
        case "idle":
            return `<div class='idle'>Ready</div>`;
        default:
            const exhaustiveCheck: never = response;
            return exhaustiveCheck;
    }
}

פתרון תרגיל 6

א. isString:

function isString(value: unknown): value is string {
    return typeof value === "string";
}

let x: unknown = "hello";
if (isString(x)) {
    console.log(x.toUpperCase()); // ok - TS knows x is string
}

ב. isNonEmptyArray:

function isNonEmptyArray(value: unknown): value is unknown[] {
    return Array.isArray(value) && value.length > 0;
}

ג. isAdmin:

interface User {
    name: string;
    email: string;
}

interface AdminUser extends User {
    permissions: string[];
}

function isAdmin(user: User | AdminUser): user is AdminUser {
    return "permissions" in user;
}

let user: User | AdminUser = {
    name: "Alice",
    email: "alice@example.com",
    permissions: ["read", "write"]
};

if (isAdmin(user)) {
    console.log(user.permissions); // ok - TS knows user is AdminUser
}

ד. סינון מערך מעורב:

let data: (string | number | null | undefined)[] = ["hello", 42, null, "world", undefined, 7, null];

function isStringValue(value: string | number | null | undefined): value is string {
    return typeof value === "string";
}

function isNumberValue(value: string | number | null | undefined): value is number {
    return typeof value === "number";
}

function isNotNullish(value: string | number | null | undefined): value is string | number {
    return value !== null && value !== undefined;
}

let strings = data.filter(isStringValue);     // string[] -> ["hello", "world"]
let numbers = data.filter(isNumberValue);     // number[] -> [42, 7]
let defined = data.filter(isNotNullish);      // (string | number)[] -> ["hello", 42, "world", 7]

פתרון תרגיל 7

א. AppEvent:

interface ClickEvent {
    type: "click";
    x: number;
    y: number;
}

interface KeypressEvent {
    type: "keypress";
    key: string;
    ctrlKey: boolean;
}

interface ScrollEvent {
    type: "scroll";
    scrollTop: number;
    scrollLeft: number;
}

interface ResizeEvent {
    type: "resize";
    width: number;
    height: number;
}

type AppEvent = ClickEvent | KeypressEvent | ScrollEvent | ResizeEvent;

ב. EventHandler:

type EventHandler = (event: AppEvent) => void;

ג. describeEvent:

function describeEvent(event: AppEvent): string {
    switch (event.type) {
        case "click":
            return `Click at (${event.x}, ${event.y})`;
        case "keypress":
            let prefix = event.ctrlKey ? "Ctrl+" : "";
            return `Key pressed: ${prefix}${event.key}`;
        case "scroll":
            return `Scrolled to (${event.scrollLeft}, ${event.scrollTop})`;
        case "resize":
            return `Resized to ${event.width}x${event.height}`;
        default:
            const exhaustiveCheck: never = event;
            return exhaustiveCheck;
    }
}

ד. filterEvents:

function isClickEvent(event: AppEvent): event is ClickEvent {
    return event.type === "click";
}

function filterEvents<T extends AppEvent>(
    events: AppEvent[],
    eventType: T["type"]
): T[] {
    return events.filter((e): e is T => e.type === eventType);
}

// usage
let events: AppEvent[] = [
    { type: "click", x: 10, y: 20 },
    { type: "keypress", key: "Enter", ctrlKey: false },
    { type: "click", x: 50, y: 100 },
    { type: "resize", width: 1920, height: 1080 }
];

let clicks = filterEvents<ClickEvent>(events, "click");
// clicks: ClickEvent[] -> [{ type: "click", x: 10, y: 20 }, { type: "click", x: 50, y: 100 }]

הערה: הגרסה הגנרית של filterEvents מתקדמת יותר. גרסה פשוטה יותר:

function filterClickEvents(events: AppEvent[]): ClickEvent[] {
    return events.filter((e): e is ClickEvent => e.type === "click");
}