沒有 TypeScript 做不到的,只有你想不到的。
泛型搭配 extends
範例一
1 | function getFirstElement<T extends number>(arr: T[]): T { |
extends
在這邊的意思是指「T
至少要滿足 number
這個 type」,而 arr
的 type 是 T[]
,把兩個組合起來的意思就是:
傳入的值必須是「陣列」,且陣列中的元素必須為「number」。
所以用起來會是這樣:
1 | getFirstElement([1, 2, 3, 4]) |
如果傳入不是 number
的 type 時就會編譯錯誤:
如果想要讓 T
也支援 string
的話可以改成這樣子:
1 | function getFirstElement<T extends number | string>(arr: T[]): T { |
這樣的意思就會變成:
傳入的值必須是「陣列」,且陣列中的元素必須為「number」或「string」。
所以現在傳入 string
陣列的話就不會出錯了:
1 | getFirstElement([1, 2, 3, 4]) // correct ✅ |
範例二
假設現在有一個印出名字的 function,長這樣:
1 | function logPersonName(person) { |
然而當我們對這個 function 定義 type 時會碰到一個問題,因為通常 person
可能會有各式各樣的屬性,像這樣:
1 | type Person1 = { |
所以如果要針對不同的 person 去定義 type 時,就得各別寫一個 function 來處理:
1 | // 給 Person1 用的 function |
看到這邊應該會覺得這是很麻煩的做法吧?所以這時候聰明的你就會想說「阿,我可以用泛型來處理吧!」,接著寫出這樣的東西:
1 | function logPersonName<T>(person: T): void { |
然後就會看到這段錯誤訊息:
這裡有特別把 any
給框起來,是因為這就是主要原因。TS 的意思是說:
我知道你給了一個泛型,但是這個泛型的範圍實在是太「廣『泛』了」,因此我無法保證
firstName
和lastName
會出現在 person 中。
為了讓 TS 確保 person
至少會有 firstName
和 lastName
這兩個屬性,我們可以用 extends
改寫成這樣子:
1 | type Person = { |
這樣子就可以確保 firstName
和 lastName
存在,但又不會僅限於特定幾個 person 才能使用這個 function 了:
1 | // correct ✅ |
範例三
如果要對 Type Alias 本身來限制泛型的話,可以這樣做:
1 | // 至少要出現的屬性 |
現在這個 GenericPerson
的用途就是:
給我一個 type(T),我會回傳你一個新的 type。傳進來的 type 可以包含任何屬性,但前提是至少要有
firstName
和lastName
屬性,否則不給過。
實際用起來會像這樣:
1 | type MyPerson = { |
如果現在把其中一個屬性拿掉的話,就會看到錯誤訊息:
這邊會看到兩個錯誤。
第一個是 person.lastName
,這是因為 MyPerson
中並沒有 lastName
,所以自然不該去存取這個屬性。
第二個是 GenericPerson<MyPerson>
,因為 GenericPerson
中的 T
有用 extends
來限制傳入的 T
至少要有 firstName
和 lastName
兩個屬性,而 MyPerson
中沒有 lastName
,所以就會出錯。
keyof 的使用
如果你有一個 type 長這樣:
1 | type Person = { |
這時候你只想要 Person
中的 key 的話可以用 keyof
來萃取:
1 | type Person = { |
這邊你可能會有一個疑惑是為什麼不會顯示 'firstName' | 'lastName'
而是 keyof Person
?這是因為在定義一個物件的 type 時它的 key 有可能會是 string
、number
或 symbol
這三種類別:
這樣子 TS 就無法確保型別是什麼,所以才無法直接顯示 'firstName' | 'lastName'
。
要解決這個問題的話可以搭配 & string
來讓 TS 知道這邊的 key 是只會是 string
就行了:
Indexd accessing
如果你有一個 type 長這樣:
1 | type Person = { |
這時候你想把 firsName
的 type(string
) 取出來的話,可以這樣做:
透過這種方式產生的 type 就稱為「Indexd accessing」。
如果想要取出「多個值」的話可以這樣寫:
接下來你要問說「如果我想要所有的值呢」,對嗎?在講解答前可以先想想看剛剛的邏輯,當我們想要取出 firstName
的 type 時會寫 Person['firstName']
,想要 firstName
和 age
時會寫 Person['firstName' | 'age']
,…以此類推。
也就是說,只要把所有 Person
的 key
都放到 []
裡面的話就可以拿到所有屬性的 type。
有察覺到在暗示什麼嗎?其實就是前面的 keyof
,只要寫成這樣就可以取出所有屬性的 type 了:
generic & keyof & indexed access 綜合練習
這邊是測試你對前面的東西有沒有熟悉,只要懂這三樣東西後就能做出蠻方便的東西。
這裡要實作的是一個用來取出物件屬性值的 function,用起來會像這樣:
1 | const person = { |
但我希望除了基本的功能以外,它還要有底下的功能:
1. 避免傳入不正確的 key:
2. 可以自動顯示能傳入哪些值:
這功能其實還蠻方便的吧?來看看這是怎麼做出來的。
首先這個 function 原本應該是長這樣子:
1 | function getObjectValue(obj: any, key: any) { |
一開始可以先不用考慮泛型的部分,我們先做簡單一點的版本就好。
如果只是單純想把 obj
跟 key
限制在一定某個範圍的話,我們只要建立一個實際的 type 來限制即可,像這樣:
1 | // 建立一個 Person type |
這樣子其實就能實現上面的兩種功能了,現在第二個參數只能傳入 firstName
、lastName
、age
,並且有自動提示的功能。
問題在於今天如果想傳入的 obj
不是 Person
這個 type 的話就沒辦法用了:
(改傳入另一個叫做 car
的物件)
雖然可以幫每一個物件建立一個不同的 function 來處理,但那樣子有點不切實際,畢竟邏輯明明都一樣。
這時候泛型就很好用了,既然 obj
會隨著傳入的值而改變,我們就把會「改變」的地方變成泛型就好了:
1 | type Person = { |
這樣就不會出現錯誤訊息了:
做到這邊其實就完成一開始想要的功能了,我們可以傳入任何物件,並且只能傳入對應的 key,也能有提示的功能。
幫 key 加上限制
如果現在想要把可以傳入的 key 加上限制,像這樣子:
1 | // 只能傳入 'age' 這個 key |
這邊給幾個提示,可以先自己思考看看後再往下看解答:
- 需要第二個泛型,用來當作 key 的 type
- 第二個泛型必須要有一定的限制,否則會因為太廣泛而出問題
1 | // 加入第二個泛型 U,並且限制 U 只能傳入 T 的 key |
稍微解釋一下這邊的流程:
- 先把 key 的 type 變成 U,讓 key 變成是可以自訂的 type
- 利用 extends 把 U 的範圍限制在 T 的 key,以免傳入不存在 T 身上的 key
最後可以注意到回傳值的部分:T[U]
。
因為我們最後會回傳的是「T 這個物件的 value」,而如果要表示這個 value 的 type 的話會用 T[...]
來表示,例如 T['firstName']
會是 string
,T['age']
會是 number
。只是現在我們把 [...]
的部分也變成是泛型了,所以就會變成 T[U]
。
這一段可能有點抽象,建議可以回憶一下之前的範例並練習看看會比較好理解一點。
Utility Type
在寫專案的時候我們通常會有一種 function 是「把某個值丟進去,再輸出成想要的結果」,這種東西就稱為「Utility」。而這種觀念也可以套用到 TS 上,只是會變成「把某個 type 丟進去,再輸出成另外一個 type」。
底下附上幾個簡單的範例,只要前面的觀念有弄懂應該就不會覺得複雜:
1 | // sample |
每個 utility 會輸出的結果:
OrNull
某個 type 或是 nullOneOrMany
單一或陣列形式的 typeOneOrManyNull
單一或陣列形式或 null 的 typeKeys
只取出 key 的 typeValues
只取出屬性值的 typePickObj
取出指定屬性值的 type
Conditional Types
在 TS 中可以像 JS 一樣用三元運算子來計算出最後的值,只是一個是回傳「值」,一個是回傳「型別(type)」。
來看個簡單的範例:
1 | // SomeType1 的型別是 'PeaNu' 這個字串 |
出來的結果會是這樣:
這段如果還記得 extends
的用途應該就不難理解,意思是「如果 SomeType1
的型別有出現在 string
的子集合裡,就回傳 true
來當作新的型別,反之則回傳 false
。
而 PeaNu
雖然是一個固定的字串值,但它當然符合 string
這個條件,所以最後的型別就會是 true
。
Flatten
接著是 TS 官方提供的一個範例:
跟剛剛的範例差不多,但可能會有疑惑的地方是 T[number]
,這個先不用的心,後面會再回來解釋。
把這段型別翻成白話的意思是「如果傳進來的 T
是陣列型別,就回傳 T[number]
,否則直接回傳原本的 T
。
所以我們可以來做個測試:
1 | type Flatten<T> = T extends any[] ? T[number] : T |
結果就會跟剛剛預期的一樣,會是 T
原本的型別:
那如果現在改傳入陣列型別呢?
1 | type Flatten<T> = T extends any[] ? T[number] : T |
會發現陣列被「拿掉了」:
這個其實就是 Flatten
的真正的用途:「把陣列中的元素型別抽出來」。
不太懂的話可以再來看個例子:
1 | type Flatten<T> = T extends any[] ? T[number] : T |
輸出結果:
關於 T[number]
搞懂了上面的 Conditional Type 後,現在回來談一下 T[number]
的部分。剛剛你可能會有疑惑的地方是「為什麼用了 T[number]
就可以把陣列元素的型別拿出來?」
這是因為當一個型別是陣列時,他的 index 值一定會是「數字」,所以我們可以透過這些數字來取出對應的元素:
1 | type Manufacture = ['a', false, null] |
但當我們想要指向陣列中的所有元素時不會是一個特定的數字,所以這時候只要把 index 指定為 number
即可:
1 | type ManufactureAll = Manufacture[number] // false | "a" | null |
分配律的概念
我們先來看一個官方提供的 utility:Extract
,它的原始碼如下:
意思是說「當 T
為 U
的子集合時,回傳 T
的型別,反之回傳 never
」。
實際使用起來會像這樣子:
1 | type Collection = 'a' | 'b' |
先來看 T1
,能看到 a
和 b
因為有出現在 Collection
的集合中,所以會被留下,而 c
因為不屬於 Collection
的集合所以不會被留下。如果這段有理解的話應該就能看懂 T2
和 T3
,因為都是相同的概念。
可是你仔細想想後會發現一道瑕疵,「為什麼前面明明說符合條件時就回傳 T
的型別(a | b | c
),但最後回傳的卻只有 a | b
?說好的 c
呢?」
這是因為當泛型傳入的型別為 union type 時,會出現類似分配律的行為。如果忘記什麼是分配律的話,這裡幫你複習一下:
1 | a * (b + c) = (a * b) + (a * c) |
所以拿 T1
的例子來示範的話,其實可以拆成這樣子:
1 | // 'a' | 'b' |
T2
的話則是:
1 | // | 'a' | 'b' | never | never |
註:任何跟 never
交集的結果都還是它自己
所以這就是為什麼可以只回傳部分的 T
,因為有分配律的概念在後面。
不想要分配律的行為
如果不想要 TS 使用分配律的行為來進行判斷的話,可以加上 []
來改寫:
1 | type ExtractNoDistribute<T, U> = [T] extends [U] ? T : never |
現在只有當 T
全部都符合條件時才會回傳整個 T
,否則一率回傳 never
。
Infer
Infer 是一個讓 TS 自動推導型別的關鍵字。這邊會拿前面介紹過的 Flatten
來示範如何使用:
原本在 Flatten
對 T
的限制是「符合任何型別的陣列」,但現在可以利用 infer
來自動推導出該陣列的型別為何:
如果你對於這邊的 (infer R)[]
有點疑惑的話,可以回憶一下你在幫陣列定義 type 的時候都是怎麼寫的,通常會是這樣子:
1 | string[] |
而現在我們希望把 []
前面的 string
、number
交給 TS 去自動判斷,所以才會用 (infer R)
的方式來告訴 TS:「我要你幫我自動推導出 R 的型別」。
接著我們在用的時後就會是這樣:
1 | // 此時的 R 等於 'PeaNu' | 'PPB' | 'Andy' |
以這個例子來說傳入的型別都有符合「陣列子集合」的條件,所以最回傳的就會是 R
的型別。
1 | type Persons = ['PeaNu', 'PPB', 'Andy'] |
另一個範例
剛剛的範例可能看不太出來 infer 的用途,所以這邊再舉一個例子:
關於這個內容我會這樣子來解釋:「T
必須是 {response: ..., status: number}
的子集合,若符合的話就回傳 ...
,反之則回傳 any
」。
而 ...
的部分我們用了 infer R
,也就是請 TS 自動推導出 R 的型別,例如說:
1 | type RespType1 = InferResponse<{ response: { data: 'PeaNu' }; status: 200 }> // { data: 'PeaNu' } |
RespType1
的內容因為有符合 extends
的條件,所以會回傳 R 的型別。而 R 的型別會依據 response
中的內容來判斷,應該能看出是 { data: 'PeaNu' }
,所以這就會變成最後的結果。
RespType2
的內容因為沒有符合 extends
的條件,所以會直接回傳 any
型別。
使用 infer 的時機與注意事項
看了前面兩個範例以後應該能察覺 infer
通常使用在需要條件判斷,但又不確定型別時使用。
此外,還有幾個必須要遵循的條件:
infer
只能用在 condition type 中的extends
和?
前的位置使用,不可以在一般的 genericextends
中使用。infer R
的這個 R 若要當成回傳值時只能在true
的情況(即?
後面),不能在false
的情況(即:
後面)下使用。
試著理解 ReturnType 和 Parameters
如果前面都有理解的話,這兩個 type 應該就不難理解。
先來看 ReturnType
的原始碼:
因為比較複雜,所以這邊可以拆成兩個部分來看:
1 | <T extends (...args: any) => any> |
首先傳入的 T
必須符合「function 的子集合,且該 function 可以接收任何參數、任何回傳值」。
1 | T extends (...args: any) => infer R ? R : any |
接著是條件判斷。若傳入的 T
符合條件(剛剛上面提的),那就把該 function 的回傳值用infer
推導出來後回傳(即 R
),反之則回傳 any
。
實際使用起來會是這樣:
1 | type FuncReturnType1 = ReturnType<(a: number, b: string) => number> // number |
接著來看 Parameters
的原始碼:
其實跟剛剛很類似,不過我們一樣拆成兩部分來看:
1 | T extends (...args: any) => any |
首先傳入的 T
必須符合「function 的子集合,且該 function 可以接收任何參數、任何回傳值」,到目前為止都跟剛剛一樣。
1 | T extends (...args: infer P) => any ? P : never |
接著就有點不同了。若傳入的 T
符合條件(剛剛上面提的),那就把該 function 的「參數值」用infer
推導出來後回傳(即 P
),反之則回傳 never
。
所以跟剛剛的差別只在於 infer
的對象不同,一個 infer
參數,一個 infer
回傳值;以及當條件不符合時回傳 never
。
實際使用起來會是這樣:
1 | type FunctionParamsType1 = Parameters<(a: number, b: string) => number> // [a: number, b: string] |
Template Literal Types
好像沒有什麼是 TS 做不到的,就連 JS 的「Template Literal」也是。直接來看範例:
就真的跟你在寫 JS 的用法幾乎一樣,不要懷疑。不過除了用來定義型別以外,在 TS 中它還有一些蠻方便的用途,下面來介紹一下。
當 Template Literal Types 碰到 Union 時
當我們在 ${}
中放入的型別是一個 union 時,它會再產生另外一組新的 union,來看範例:
所以來考考你,底下的範例會輸出什麼:
1 | type X = 'left' | 'right' |
答案:
1 | ;'left-top' | 'left-bottom' | 'right-top' | 'right-bottom' |
把 Enum 的 values 變成 Union Type
如果你有一個 Enum 長這樣:
當你想把這些「值」變成一個 type 時,你可能會額外寫一個 Type Alias 來處理:
1 | type ManufactureValues = 'apple' | 'samsung' | 'google' | 'sony' |
這樣子的缺點是如果未來要新增 MANUFACTURE
的內容時,就得同步更新 ManufactureValues
的內容,其實還蠻麻煩的。
因此可以利用 Template Literal Types 來改寫成這樣:
1 | type ManufactureValues = `${MANUFACTURE}` // "apple" | "samsung" | "google" | "sony" |
只要把 MANUFACTURE
放入 ${}
以後就會自動產生對應的 union 出來,非常非常好用!
Recursive type
建立 Type Alias 的方式有很多種,但沒想到連「遞迴」也是有可能的!?讓我們來看個範例:
1 | type ValueOrArray<T> = T | T[] |
這裡先建立了一個 ValueOrArray
,用途是讓我們可以建立一個「純值」或「陣列值」的變數,所以 something
可以是 number
或 number[]
。
可是當出現巢狀陣列時會有問題:
其實也沒什麼奇怪的,因為我們只有說型別是 number[]
或 number
,並沒有定義 number[][]
。
所以這裡可以用遞迴的方式來處理:
1 | type ValueOrNestedArray<T> = T | ValueOrNestedArray<T>[] |
這邊的 ValueOrNestedArray
的值是 T | ValueOrNestedArray<T>[]
。要注意的地方是它在裡面又呼叫了自己,所以就會有點類似這樣的感覺:
1 | T | T[] | T[][] | T[][][] | ... |
所以用這種方式改寫後就能處理巢狀陣列的問題。
範例-SnakeToCamelCase
這是另一個遞迴的應用範例,原始碼如下:
雖然剛開始看我也覺得有點複雜,不過拆成一塊一塊來解讀就會比較好理解了。我們先來看第一個部分:
1 | type SnakeToCamelCase<T extends string> |
這段的意思是「傳入的 T 必須是 string 的子集合」。
1 | T extends `${infer Head}_${infer Tail}` ? ... : T |
這邊是一個條件判斷,「當 T
屬於 xxx_ooo
字串的子集合」時符合條件,不符合的話就回傳 T
。
此外,這邊還搭配了 infer
來推導出字串的 literal string type。舉例來說,如果傳入的 T
為 hello_my_world
,那 infer
出來的結果就會是 hello
與 my_world
(前者為 Head
,後者為 Tail
)。
1 | ;`${Uncapitalize<Head>}${Capitalize<SnakeToCamelCase<Tail>>}` |
這邊使用了兩個內建的 Utility Types:
Uncapitalize
顧名思義,把第一個字轉成小寫。Capitalize
顧名思義,把第一個字轉成大寫。
舉個範例:
1 | type CapitalizeWord1 = Uncapitalize<'Hello'> // 'hello' |
理解後讓我們繼續回到剛剛的段落,現在假設 <Head>
與 <Tail>
的值為 hello
與 my_world
的話,出來的結果會是這樣:
1 | ${'hello'}${Capitalize<SnakeToCamelCase<'My_world'>>} |
這邊如果跟我一樣對遞迴苦手的話,可以先拿掉 SnakeToCamelCase
這一段:
1 | ${'hello'}${Capitalize<'My_world'>} |
這樣子最後的結果就會是 helloMy_world
,應該不難理解:
可以看到如果沒有遞迴的話,就只會對第一段的 xxx_ooo
做轉換,後面的部分沒有處理完全。所以這個時候我們要做的事情很簡單,就是把剛剛的事情再做一次,反覆的重複這段動作,直到不會再出現 xxx_ooo
的格式為止,這個就是遞迴的用意。
這邊附上遞迴中的每個步驟的流程,希望有助於理解:
1 | 1. hello_my_world // 第一次執行 SnakeToCamelCase<'hello_my_world'> |
我覺得遞迴之所以難懂,是在於它的執行順序跟你想的不太一樣。以上面的例子來說,雖然 SnakeToCamelCase<'hello_my_world'>
是第一個被執行的,但他實際跑完的時間點卻要等到後面的 SnakeToCamelCase<'my_world'>
、 SnakeToCamelCase<'world'>
跑完並把值回傳以後,才會執行回過頭來下一行的 ${'hello'}${Capitalize( 'myWorld' )}
。
所以碰到遞迴的時候要常常用反向的方式去思考,要用「排在後面那個 function 的回傳值應該是什麼?」下去思考,我覺得這樣就會好理解一點了。
typeof
如果你寫 TS 已經有一段時間了,應該會知道 TS 有一個自動推導的功能,就是即便我們沒有幫變數指定型別,它也能自動推導出來:
這樣的好處是什麼?好處是只要用 TS 中的 typeof
(不是 JS 的那個)就能產生出新一個型別,像這樣:
注意這裡是直接檢查一個變數的型別(bio
),再把這個型別賦給 BioType
,跟以往的做法不太一樣。
既然可以用 typeof
來取出型別,那這樣子玩也是 ok 的:
1 | const bio = { |
利用 typeof 取出 Enum 的 Key
上次有介紹過如何用 Template Literal Types 來取出 Enum 的值:
1 | type ManufactureValues = `${MANUFACTURE}` // "APPLE" | "SAMSUNG" | "GOOGLE" | "SONY" |
這次來介紹如何用 typeof
來取出 Enum 的 keys:
1 | type ManufactureKeys = keyof typeof MANUFACTURE // "APPLE" | "SAMSUNG" | "GOOGLE" | "SONY" |
這兩招學起來後對 Enum 會非常有幫助!
Index Signatures & Mapped Type
一般在幫物件建立型別時會這樣子做:
1 | type Person = { |
但如果哪天你碰到「有太多屬性」的問題時,你可能不會想像上面這樣子把每一個屬性都列出來,這時候就可以改寫成這樣:
1 | type Person = { |
這種寫法叫做「Index Signatures」。我們沒有直接指定屬性名稱,而是用一個 key
來當作變數,並把這個 key
的型別設為 string
。
key
只是一個變數,你想取什麼名字都 ok。而型別的部分可以指定的值有 string | number | symbol
。當然,這邊 Person
的屬性很顯然是一個字串,所以 string
自然是最符合的型別。
在理解完 Index Signatures 以後,接著就可以來介紹「Mapped Type」。
Mapped Type 的用法跟前面很類似,只差在它會多用到一個 in
關鍵字,來看一個範例:
1 | type Person = { |
註:這裡的 key
一樣只是個變數,想取啥就取啥。
如果你熟 ES6 的話應該會覺得這跟 for...in
和 for...of
的寫法很類似。確實,這兩者背後的共通點就是「iterable」的概念。
以上面的例子來說,其實只是去迭代 firstName
和 lastName
這兩個值,並產生出像這樣的東西:
1 | type Person = { |
這個例子看起來有點脫褲子放屁的感覺,不過如果用對地方的話會發現這是一個很厲害的功能,讓我們再看一個例子。
假設目前有個 type 長這樣:
1 | type Events = { |
如果我想要再多一個一樣的型別,只是 string
的部分改成 function
,像這樣:
1 | type EventHandlers = { |
你可以想想看怎麼利用 Mapped Type 來改寫嗎?不然這樣子寫會有一個問題,就是當 A 改變時也得一併修改 B,才能確保兩邊的內容是一致的。
答案是:
1 | type HandleEvents = { |
產出的內容:
很神奇吧!但其實只是利用前面提過的概念而已,先用 keyof
把 Events
的屬性取出來,再利用 Mapped Type 來迭代出新的型別。
搭配 Template Literal 修改 key 的值
剛剛已經介紹過怎麼利用 Mapped Type 來修改物件型別的值的 type,這邊再教你一招新的:「如何修改 key 的值」。
這邊會拿剛剛的範例來用:
1 | type HandleEvents = { |
這邊想做的事情是,把 key 的部分改成:
1 | type EventHandlers = { |
如果要達成這樣的效果,可以建立一個 utility type 來達成:
首先來看 K in key of T & string
這一段。因為我們要對傳入的型別(即 T
)的 key 做修改,所以必須先把原本的 key 透過 keyof
取出。
另外 T & string
後面 & string
的用途在前面有提過,一個 key 的型別可以是 string | number | symbol
,所以要用 & string
來讓 TS 知道這邊的 key 是 string
,否則後面在使用時會有一些衝突。
順利取出 key
以後就可以搭配 in
來迭代每一個 key。
接著看到 as ...
的部分。as
的作用是「斷言」,例如說:
1 | const something = '123' as number |
這裡的 '123'
照理說會是 string
,但是當我們在後面加上 as number
以後,something
就會被強制變成 number
的型別:
我知道這個例子看起來有點詭異,但重點是要強調 as
可以把原本的型別修改成另一個型別。
所以 K in key of T & string
後面的 as
就是在「把前面的 K
修改成 ???
型別」。這裡想做的修改是把原本的 K
加上前綴字 handler
,所以搭配了 Template Literal 來做處理(即 handler${Capitalize<K>}
),透過這種方式產出的型別就會是 handler(xxx)
(xxx
就是 K
原本的字串內容)。
除了加上 handler
前綴以外,這邊還用了 Capitalize
把 K
的字首變成大寫,藉此讓產出的文字符合 JS 的命名慣例(駝峰式)。
理解完每一步的邏輯後,現在可以利用它來達到我們想要的結果了:
1 | type ToEventHandler<T> = { |
Property modifiers
雖然你可能不知道什麼是「Property modifiers」,但你其實早就在用它了:
1 | type Person = { |
?
跟 readonly
這兩個符號都是 Property modifiers。如其名,就是 property 的修飾符號。以這個例子來說的話:
age
屬性是 optional 的,沒有也沒關係lastName
的屬性值只能讀,不可以改寫它的值
搭配 Mapped Type 來使用
如果現在我們有兩個 Type 是:
1 | type OptionalPerson = { |
現在如果要讓 OptionalPerson
的所有屬性變成 Optional,可以這樣寫:
1 | type ToOptional<T> = { |
或者是把 NonOptionalPerson
的所有屬性變成 Required 的話:
1 | type ToRequired<T> = { |
又或者是設為 readonly
或 non-readonly
的話也可以這樣做:
1 | type ToNonReadOnly<T> = { |
最後來做個測試。
如果現在把 OptionalPerson
丟給 ToRequired
,所有屬性就會變成 Required:
Partial、Required 和 Readonly
剛剛利用 property modifier 建立的三種 Type,其實就是 TS 內建提供的 Partial
、Required
和 Readonly
。這邊分別來看一下它們原始碼:
1 | type Partial<T> = { |
1 | type Required<T> = { |
1 | type Readonly<T> = { |
泛型參數預設值
如果你有用過 JS 中的參數預設值的話,要理解 TS 中的泛型參數預設值應該就不困難。
我們一般在 JS 中使用參數預設值的時候通常是這樣:
1 | // 如果 arr 沒有傳,將預設值設為空陣列 |
要在 TS 中做一樣的事情也是 OK 的,讓我們回到一開始的例子:
1 | type PickWithDefaultValue<T, K extends keyof T = keyof T> = Pick<T, K> |
這裡的 PickWithDefaultValue
跟原本的 Pick
功能是一樣的,會接收兩個參數,第一個參數是原本的物件型別,第二個參數是要取出的屬性。
兩個的差別只在於第二個參數是不是一定要傳入?以原本 Pick
來說,如果你沒有傳入第二個參數的話會 TS 會報錯:
但如果是用 PickWithDefaultValue
的話,因為有預設值的關係,所以就算不傳入第二個參數也沒關係:
這是怎麼做到的?只是因為我們幫他加上了預設值而已:
1 | // 如果 K 沒有值,就設為 T 的所有 key |