לדלג לתוכן

5.5 טיפוסי איחוד וחיתוך פתרון

פתרון - טיפוסי איחוד וחיתוך

פתרון תרגיל 1

א. describeInput:

type Input = string | number | boolean;

function describeInput(value: Input): string {
    if (typeof value === "string") {
        return `string: ${value}`;
    }
    if (typeof value === "number") {
        return `number: ${value}`;
    }
    return `boolean: ${value}`;
}

ב. Nullish ו-unwrap:

type Nullish<T> = T | null | undefined;

function unwrap(value: Nullish<string>, defaultValue: string): string {
    if (value === null || value === undefined) {
        return defaultValue;
    }
    return value;
}

// or shorter:
function unwrap2(value: Nullish<string>, defaultValue: string): string {
    return value ?? defaultValue;
}

ג. toArray:

type StringOrArray = string | string[];

function toArray(value: StringOrArray): string[] {
    if (typeof value === "string") {
        return [value];
    }
    return value;
}

// or shorter:
function toArray2(value: StringOrArray): string[] {
    return Array.isArray(value) ? value : [value];
}

פתרון תרגיל 2

א-ב. Suit, Rank, Card:

type Suit = "hearts" | "diamonds" | "clubs" | "spades";
type Rank = 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | "J" | "Q" | "K" | "A";

interface Card {
    suit: Suit;
    rank: Rank;
}

ג. getCardValue:

function getCardValue(card: Card): number {
    if (typeof card.rank === "number") {
        return card.rank;
    }
    switch (card.rank) {
        case "J":
        case "Q":
        case "K":
            return 10;
        case "A":
            return 11;
    }
}

ד. formatCard:

function formatCard(card: Card): string {
    return `${card.rank} of ${card.suit}`;
}

פתרון תרגיל 3

א. טיפוסי תשלום:

interface CreditCardPayment {
    method: "creditCard";
    cardNumber: string;
    expiryDate: string;
    cvv: string;
}

interface PayPalPayment {
    method: "paypal";
    email: string;
}

interface BankTransferPayment {
    method: "bankTransfer";
    bankName: string;
    accountNumber: string;
    routingNumber: string;
}

interface CryptoPayment {
    method: "crypto";
    walletAddress: string;
    currency: "BTC" | "ETH" | "USDT";
}

type Payment = CreditCardPayment | PayPalPayment | BankTransferPayment | CryptoPayment;

ב. getPaymentSummary:

function getPaymentSummary(payment: Payment): string {
    switch (payment.method) {
        case "creditCard":
            let last4 = payment.cardNumber.slice(-4);
            return `Credit card ending in ${last4}`;
        case "paypal":
            return `PayPal: ${payment.email}`;
        case "bankTransfer":
            return `Bank transfer: ${payment.bankName}`;
        case "crypto":
            let shortAddress = payment.walletAddress.slice(0, 8) + "...";
            return `${payment.currency}: ${shortAddress}`;
        default:
            const exhaustiveCheck: never = payment;
            return exhaustiveCheck;
    }
}

ג. getProcessingFee:

function getProcessingFee(payment: Payment, amount: number): number {
    switch (payment.method) {
        case "creditCard":
            return amount * 0.029;
        case "paypal":
            return amount * 0.035;
        case "bankTransfer":
            return amount * 0.01;
        case "crypto":
            return amount * 0.005;
    }
}

פתרון תרגיל 4

א. Result:

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

ב. parseJSON:

function parseJSON(input: string): Result<unknown> {
    try {
        let data = JSON.parse(input);
        return { success: true, value: data };
    } catch (e) {
        let message = e instanceof Error ? e.message : "Unknown error";
        return { success: false, error: message };
    }
}

let good = parseJSON('{"name": "Alice"}');
if (good.success) {
    console.log(good.value); // { name: "Alice" }
}

let bad = parseJSON("not json");
if (!bad.success) {
    console.log(bad.error); // error message
}

ג. divide:

function divide(a: number, b: number): Result<number> {
    if (b === 0) {
        return { success: false, error: "Division by zero" };
    }
    return { success: true, value: a / b };
}

ד. chain:

function chain<T, U>(result: Result<T>, fn: (value: T) => Result<U>): Result<U> {
    if (!result.success) {
        return result; // pass through the failure
    }
    return fn(result.value);
}

// usage: parse JSON, then divide a value
let result = chain(
    parseJSON('{"value": 10}'),
    (data) => {
        let obj = data as { value: number };
        return divide(obj.value, 3);
    }
);

פתרון תרגיל 5

א. טיפוסי traits:

type Identifiable = {
    id: string;
};

type Timestamped = {
    createdAt: Date;
    updatedAt: Date;
};

type Ownable = {
    ownerId: string;
    ownerName: string;
};

ב. טיפוסים מורכבים:

type Document = {
    title: string;
    content: string;
} & Identifiable & Timestamped & Ownable;

type Comment = {
    text: string;
    parentId: string;
} & Identifiable & Timestamped;

type Tag = {
    name: string;
    color: string;
} & Identifiable;

ג. getAge:

function getAge(item: Timestamped): number {
    let now = new Date();
    let diff = now.getTime() - item.createdAt.getTime();
    return Math.floor(diff / (1000 * 60 * 60 * 24));
}

// works with Document, Comment, or anything with Timestamped
let doc: Document = {
    id: "1",
    title: "Hello",
    content: "World",
    createdAt: new Date("2025-01-01"),
    updatedAt: new Date("2025-06-01"),
    ownerId: "u1",
    ownerName: "Alice"
};

console.log(getAge(doc)); // number of days since 2025-01-01

פתרון תרגיל 6

א. PlayerState:

interface Stopped {
    state: "stopped";
}

interface Playing {
    state: "playing";
    track: string;
    position: number;
    volume: number;
}

interface Paused {
    state: "paused";
    track: string;
    position: number;
    volume: number;
}

interface Buffering {
    state: "buffering";
    track: string;
    progress: number;
}

type PlayerState = Stopped | Playing | Paused | Buffering;

ב. getPlayerDisplay:

function getPlayerDisplay(state: PlayerState): string {
    switch (state.state) {
        case "stopped":
            return "Player stopped";
        case "playing":
            return `Playing: ${state.track} (${state.position}s) - Volume: ${state.volume}%`;
        case "paused":
            return `Paused: ${state.track} at ${state.position}s`;
        case "buffering":
            return `Buffering: ${state.track} (${state.progress}%)`;
    }
}

ג. handlePlayerAction:

type PlayerAction =
    | { type: "play"; track: string }
    | { type: "pause" }
    | { type: "stop" }
    | { type: "seek"; position: number };

function handlePlayerAction(state: PlayerState, action: PlayerAction): PlayerState {
    switch (action.type) {
        case "play":
            return {
                state: "buffering",
                track: action.track,
                progress: 0
            };

        case "pause":
            if (state.state === "playing") {
                return {
                    state: "paused",
                    track: state.track,
                    position: state.position,
                    volume: state.volume
                };
            }
            if (state.state === "paused") {
                return {
                    state: "playing",
                    track: state.track,
                    position: state.position,
                    volume: state.volume
                };
            }
            return state;

        case "stop":
            return { state: "stopped" };

        case "seek":
            if (state.state === "playing") {
                return { ...state, position: action.position };
            }
            if (state.state === "paused") {
                return { ...state, position: action.position };
            }
            return state; // can't seek when stopped or buffering
    }
}

פתרון תרגיל 7

א. טיפוסים:

type BaseMessage = {
    id: string;
    timestamp: number;
    senderId: string;
};

type TextMessage = BaseMessage & {
    type: "text";
    content: string;
};

type ImageMessage = BaseMessage & {
    type: "image";
    url: string;
    width: number;
    height: number;
    caption?: string;
};

type FileMessage = BaseMessage & {
    type: "file";
    fileName: string;
    fileSize: number;
    mimeType: string;
};

type SystemMessage = BaseMessage & {
    type: "system";
    action: "join" | "leave" | "rename";
    details: string;
};

type ChatMessage = TextMessage | ImageMessage | FileMessage | SystemMessage;

ב. renderMessage:

function renderMessage(message: ChatMessage): string {
    switch (message.type) {
        case "text":
            return `<div class="message text"><p>${message.content}</p></div>`;
        case "image":
            let caption = message.caption ? `<p class="caption">${message.caption}</p>` : "";
            return `<div class="message image"><img src="${message.url}" width="${message.width}" height="${message.height}" />${caption}</div>`;
        case "file":
            let sizeMB = (message.fileSize / (1024 * 1024)).toFixed(2);
            return `<div class="message file"><a href="#">${message.fileName}</a> (${sizeMB} MB)</div>`;
        case "system":
            return `<div class="message system"><em>${message.details}</em></div>`;
    }
}

ג. searchMessages:

function searchMessages(messages: ChatMessage[], query: string): ChatMessage[] {
    let lowerQuery = query.toLowerCase();

    return messages.filter(message => {
        switch (message.type) {
            case "text":
                return message.content.toLowerCase().includes(lowerQuery);
            case "image":
                return message.caption?.toLowerCase().includes(lowerQuery) ?? false;
            case "file":
                return message.fileName.toLowerCase().includes(lowerQuery);
            case "system":
                return message.details.toLowerCase().includes(lowerQuery);
        }
    });
}

ד. getMessageStats:

interface MessageStats {
    textCount: number;
    imageCount: number;
    fileCount: number;
    systemCount: number;
    totalFileSize: number;
}

function getMessageStats(messages: ChatMessage[]): MessageStats {
    let stats: MessageStats = {
        textCount: 0,
        imageCount: 0,
        fileCount: 0,
        systemCount: 0,
        totalFileSize: 0
    };

    for (let message of messages) {
        switch (message.type) {
            case "text":
                stats.textCount++;
                break;
            case "image":
                stats.imageCount++;
                break;
            case "file":
                stats.fileCount++;
                stats.totalFileSize += message.fileSize;
                break;
            case "system":
                stats.systemCount++;
                break;
        }
    }

    return stats;
}