TypeScript type 的巧妙運用

tags: TypeScript
category: Front-End
description: ``
created_at: 2023/03/14 20:00:00

cover image


前言

也許這篇的內容對於型別大師(?)會覺得這很基本,但我覺得這很帥,就想記錄一下


事情是這樣的

在好幾個月前,我在寫 Vue 的時候,因為 router 可以設定 pathname,而抽離到常數的東西頂多是 path,像是下面這樣

const routes = {
    home: '/',
    dashboard: '/dashboard'
}

頂多加一點巢狀,讓他更符合 children routes

const routes = {
    home: '/',
    pages: {
        index: '/pages',
        page1: '/page1',
        page2: '/page2'
    }
}

但這時又引發了另一個問題,如果我希望 page1page2 都是基於 /pages 這個前綴,就會重複寫很多遍一樣的東西,變成下面這樣:

const routes = {
    home: '/',
    pages: {
        index: '/pages',
        page1: '/pages/page1',
        page2: '/pages/page2'
    }
}

而且這時如果我的路由有使用到路由參數(params),可能要在 router 中的每個 routes 都定義 name,這樣常數只存 path 並沒有辦法處理好(雖然也可以暴力帶入 path 當作 name但又感覺好像語意怪怪)。

總之這時我就想說能不能把 routes 這個物件的字串(看做樹狀結構就是葉子)都轉換成一個物件,而新的物件包含 namepath,這樣前綴也可以一併處理掉。

如果是 JavaScript 一定會想說廢話,當然可以。


先來宣告葉子的型態

interface RouteValue {
  path: string
  name: string
}

這個型態包含 pathname,而我給他是使用 interface 宣告,而不是 type

我自己認為這邊比起 type 更適合使用 interface,這邊列出其中一些理由

  1. 語意上 type 比較像宣告一般型態(如stringnumber等等,說白了其實 type 也只是 alias),interface 比較像宣告物件型態
  2. type 沒有辦法被重複宣告來擴充,這裡我不希望把 RouteValue 寫死
  3. 第一點如果拿 Vue3 來比喻,我覺得有一點點像是 refreactive 的關係

定義完葉子的型態後,接下來希望把所有的葉子型態做轉換,這時候就誕生了一個神奇的 type

type ReplaceDeep<T, A, B> = {
  [K in keyof T]: T[K] extends A
    ? B
    : T[K] extends object
      ? ReplaceDeep<T[K], A, B>
      : T[K]
}

在搭配上下面這個使用方式

type Routes<T> = ReplaceDeep<T, string, RouteValue>

寫過 OOP 相關的語言應該可以很直接的猜到那個 <> 中間的東西就是泛型,ReplaceDeep 這個 type 吃三個泛型,他可以把 T 型態當中的 A 型態轉換為 B 型態。

翻譯成虛擬碼大概會長的下面這樣

function ReplaceDeep(T, A, B) {
    for (let K in T) {
        if (typeof T[K] === A) {
            return B
        } else {
            if (typeof T[K] === Object) {
                return ReplaceDeep(T[K], A, B)
            } else {
                return T[K]
            }
        }
    }
}

不要嘗試想去跑上面那一段(因為我也沒跑過),主要是理解那個 type 的邏輯。

可以成功轉換型態之後,這時問題又來了,那個 T 怎麼來?

很簡單,直接透過 typeof 搞定。

其實這一篇就是 我造的輪子 - vue-router-auto-complete 的誕生過程XD




最後更新時間: 2023年03月14日.