iT邦幫忙

2024 iThome 鐵人賽

DAY 29
0
JavaScript

我推的TypeScript 操作大全系列 第 29

我推Day29 - 從可變元組到 Opaque Types,揭開 TypeScript 型別系統的大心法

  • 分享至 

  • xImage
  •  

使用泛型創建型別安全的事件發射器 🎉

在事件驅動的程式碼中,創建型別安全的事件發射器(Event Emitters)能夠大幅提升程式的可靠性。透過使用 TypeScript 的泛型功能,我們可以確保事件名稱與相對應的資料型別始終保持一致,避免不匹配的錯誤。

例子:

type Listener<T> = (event: T) => void;

class TypedEventEmitter<EventMap extends Record<string, any>> {
    private listeners: { [K in keyof EventMap]?: Listener<EventMap[K]>[] } = {};

    on<K extends keyof EventMap>(event: K, listener: Listener<EventMap[K]>) {
        if (!this.listeners[event]) {
            this.listeners[event] = [];
        }
        this.listeners[event]!.push(listener);
    }

    emit<K extends keyof EventMap>(event: K, data: EventMap[K]) {
        this.listeners[event]?.forEach(listener => listener(data));
    }
}

假設我們要處理使用者登入和資料加載的事件,我們可以這樣定義:

interface MyEvents {
    userLoggedIn: { userId: string; timestamp: number };
    dataLoaded: { items: string[] };
}

const emitter = new TypedEventEmitter<MyEvents>();

emitter.on("userLoggedIn", ({ userId, timestamp }) => {
    console.log(`使用者 ${userId} 在 ${timestamp} 時登入`);
});

emitter.emit("userLoggedIn", { userId: "123", timestamp: Date.now() }); // OK
// emitter.emit("userLoggedIn", { userId: "123" }); // 錯誤:缺少 'timestamp' 屬性
// emitter.emit("invalidEvent", {}); // 錯誤:'invalidEvent' 不是 MyEvents 中的事件

這種模式確保你在事件驅動的程式中,不會因為事件名稱或資料結構的不匹配而發生錯誤。應用場景如:如果你正在開發一個多模組的大型應用程式,這種方式可以幫助你清晰地定義每個模組所需的事件和資料結構,保證跨模組的溝通保持一致性。


自參考型別:建構遞迴資料結構 🌲

自參考型別(Self-Referencing Types)在處理遞迴資料結構時特別有用,比如樹狀結構、文件系統或連結串列。這類型別允許型別定義自己內部包含同樣的型別,非常適合表示具有嵌套關係的資料結構。

例子:

type FileSystemObject = {
    name: string;
    size: number;
    isDirectory: boolean;
    children?: FileSystemObject[];
};

const fileSystem: FileSystemObject = {
    name: "root",
    size: 1024,
    isDirectory: true,
    children: [
        {
            name: "documents",
            size: 512,
            isDirectory: true,
            children: [
                { name: "report.pdf", size: 128, isDirectory: false },
                { name: "invoice.docx", size: 64, isDirectory: false }
            ]
        },
        { name: "image.jpg", size: 256, isDirectory: false }
    ]
};

在這個範例中,我們定義了一個模擬文件系統的型別 FileSystemObject,這個型別可以遞迴地包含其他 FileSystemObject,用來表示目錄與檔案的嵌套關係。為了計算這個文件系統中所有檔案的總大小,我們可以這樣實作:

function calculateTotalSize(fsObject: FileSystemObject): number {
    if (!fsObject.isDirectory) {
        return fsObject.size;
    }
    return fsObject.size + (fsObject.children?.reduce((total, child) => total + calculateTotalSize(child), 0) ?? 0);
}

console.log(calculateTotalSize(fileSystem)); // 輸出文件系統中所有檔案的總大小

這個技巧讓我們能夠型別安全地表示和操作複雜的嵌套資料結構,像是文件系統、組織架構、樹狀目錄等。當你在處理像 API 回應、遞迴資料或是層層嵌套的物件時,自參考型別非常有用,確保型別定義清晰且一致,讓資料結構操作起來更安心!


Opaque Types 使用唯一符號進行區隔 🛡️

Opaque types(不透明型別)提供了一種方法,可以創建結構上相似但在型別系統中被視為不同的型別。這非常適合在需要創建型別安全的識別符或防止相似型別誤用的情況下使用。透過 unique symbol,你可以區隔出結構上相似但語義上完全不同的型別。

例子:

declare const brand: unique symbol;

type Brand<T, TBrand> = T & { readonly [brand]: TBrand };

type Email = Brand<string, "Email">;
type UserId = Brand<string, "UserId">;

function createEmail(email: string): Email {
    // 真實應用中,你可以在這裡進行 email 驗證
    return email as Email;
}

function sendEmail(email: Email, message: string) {
    console.log(`發送訊息 "${message}" 給 ${email}`);
}

const email = createEmail("user@example.com");
const userId = "12345" as UserId;

sendEmail(email, "Hello!"); // OK
// sendEmail(userId, "Hello!"); // 錯誤:'UserId' 不能分配給 'Email'

在這個範例中,EmailUserId 都是基於字串的型別,但因為使用了 Brand 類型,我們可以防止兩者在錯誤的地方被混用。這個模式非常適合用於處理特定領域的型別,例如電子郵件和使用者 ID,儘管它們可能在結構上相似,但不應該被互換使用。


小結:掌握更多 TypeScript 進階技巧

📌 可變參數元組型別
可變參數元組型別讓你可以靈活處理不定長度的參數,特別是在需要合併元組或處理動態長度輸入時,能夠保持型別安全的操作。

📌 型別安全的事件發射器
透過泛型和型別映射,可以確保事件名稱與相應的資料結構匹配,防止因事件不一致引發的錯誤,適合大型應用程式的多模組事件管理。

📌 利用 'never' 型別進行鑑別聯合
鑑別聯合結合 never 型別,確保每種物件狀態都被正確處理,防止遺漏任何分支狀況,特別適合狀態機或多分支邏輯處理。

📌 自參考型別
自參考型別允許型別內部引用自身,能夠輕鬆表達嵌套資料結構,尤其適合處理遞迴資料結構如文件系統或 API 回應。

📌 Opaque Types(不透明型別)
利用唯一符號將相似結構的型別區隔開來,防止它們在錯誤的上下文中被誤用。這非常適合在處理應用中特定領域的型別,例如識別符、帳號或資料庫 ID。


無論學習多麼複雜的技術,只要堅持不懈,你的努力終將開花結果!保持熱情,勇敢探索 TypeScript 的每一個角落,未來的程式之路,因你而精彩!💪🚀


上一篇
我推Day28 - 用這 5 大進階技巧,讓 TypeScript 成為你的秘密武器
下一篇
我推Day30 - API 開發再也不怕,TypeScript 進階型別駕馭術
系列文
我推的TypeScript 操作大全30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言