TypeScript type 的巧妙運用
tags: TypeScript
category: Front-End
description: ``
created_at: 2023/03/14 20:00:00
前言
也許這篇的內容對於型別大師(?)會覺得這很基本,但我覺得這很帥,就想記錄一下
事情是這樣的
在好幾個月前,我在寫 Vue
的時候,因為 router
可以設定 path
、name
,而抽離到常數的東西頂多是 path
,像是下面這樣
const routes = {
home: '/',
dashboard: '/dashboard'
}
頂多加一點巢狀,讓他更符合 children routes
const routes = {
home: '/',
pages: {
index: '/pages',
page1: '/page1',
page2: '/page2'
}
}
但這時又引發了另一個問題,如果我希望 page1
或 page2
都是基於 /pages
這個前綴,就會重複寫很多遍一樣的東西,變成下面這樣:
const routes = {
home: '/',
pages: {
index: '/pages',
page1: '/pages/page1',
page2: '/pages/page2'
}
}
而且這時如果我的路由有使用到路由參數(params
),可能要在 router
中的每個 routes
都定義 name
,這樣常數只存 path
並沒有辦法處理好(雖然也可以暴力帶入 path
當作 name
,但又感覺好像語意怪怪)。
總之這時我就想說能不能把 routes
這個物件的字串(看做樹狀結構就是葉子)都轉換成一個物件,而新的物件包含 name
與 path
,這樣前綴也可以一併處理掉。
如果是 JavaScript
一定會想說廢話,當然可以。
先來宣告葉子的型態
interface RouteValue {
path: string
name: string
}
這個型態包含 path
、name
,而我給他是使用 interface
宣告,而不是 type
我自己認為這邊比起 type
更適合使用 interface
,這邊列出其中一些理由
- 語意上
type
比較像宣告一般型態(如string
、number
等等,說白了其實type
也只是alias
),interface
比較像宣告物件型態 type
沒有辦法被重複宣告來擴充,這裡我不希望把RouteValue
寫死- 第一點如果拿
Vue3
來比喻,我覺得有一點點像是ref
與reactive
的關係
定義完葉子的型態後,接下來希望把所有的葉子型態做轉換,這時候就誕生了一個神奇的 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