myId
這個 key 時,就會加上預設的 property,如 { myId: string }
myId
時,則會覆寫成傳入的 property type例子:
/**
* 當輸入的 object 沒有傳入 myId 這個 key 時,
* 就會加上預設的 property { myId: string }
*/
type peopleDefaultId = People<{ name: string }>
/* expected:
{
myId: string // default
name: string
}
*/
/**
* 當輸入的 object 含有 `myId` 時,
* 則會覆寫成傳入的 property type
*/
type peopleNumberId = People<{ myId: number, name: string }>
/* expected:
{
myId: number // override
name: string
}
*/
/**
* 代表一個 People 的 generic type,當輸入的 object 含有 `myId` key 時,
* 會將 `myId` property 的型別覆蓋為傳入的型別,否則將加上預設的 property `{ myId: string }`,
* 同時 `myId` key 的型別會保留為原本的 optional / non-optional 狀態。
*/
type peopleOptionalStringId = People<{ myId?: string, name: string }>
/* expected:
{
myId?: string // override
name: string
}
*/
type ExtendsDefault<T, Default> = T & Partial<Pick<Default, Exclude<keyof Default, keyof T>>>;
type People<T> = ExtendsDefault<T, {
myId: string
}>
|
符號// 錯誤示範
type People<T> = {
myId: string
} | T
如果輸入的 object 本身已經包含 myId
key 的話(如 myId: number
),
那輸出的 myId
type 最後就會變成 string | number
。
type peopleNumberId = People<{ myId: number, name: string }>
// 預期:
// {
// myId: number,
// name: string
// }
// 實際輸出:
// {
// myId: string | number, // 錯誤!
// name: string
// }
extends
來斷使用 typeinterface People<T> {
myId: T extends { myId: infer U } ? U : string
}
先檢查輸入的 generic object 是否有 myId
,有就把對應的 type 抽出來,沒有就直接 default 成 string
這樣看起來好像解決了第 1 個踩坑過程的問題,但是仔細看,由於上面的 myId
不是一個 optional 的 key,所以假設我今天輸入的 generic type 是 myId?: string
這種 optional property 時,我也必須要一定要加上 myId
這個 key,這樣並不方便。
// [error] Property 'myId' is missing in type '{}' but required in type
const people: People<{ myId?: string }> = {}
^^ // error
反過來,假如今天我預設的 myId
如果是 optional 的話,那當我 define 一個 non-null property,他將會強制我的 property 變成 optional。
interface People<T> {
myId?: T extends { myId: infer U } ? U : string
}
const people: People<{ myId: string }> = {}
// output: { myId?: string } <- error: expected to be { myId: string }
type People<T extends { myId?: any }> = 'myId' extends keyof T
? T extends { myId: infer U }? { myId: U }
: T extends { myId?: infer U } ? { myId?: U } : never
: { myId: string };
這樣其實能滿足所有條件的,但感覺代碼有點又長又亂,然後後來發現可以用 [myId in keyof T]
直接拿到 myId
是否為 optional property 並拿來做 key,因此後來改善成了以下版本:
type People<T extends { myId?: any }> = 'myId' extends keyof T
? { [myId in keyof T]: T[myId] }
: { myId: string };
然後順便做了一個可以輸入自定 key
的版本,這樣之後如果 property 不是 myId
的話就可以更方便使用了。
type DefaultProperty<T extends { [K in keyof T]?: any }, K extends keyof any> = K extends keyof T
? { [K in keyof T]: T[K] }
: { [K in keyof any]: number };
// usage
type People<T, K> = DefaultProperty<T, 'myId'>
但做到這邊才發現問題就是,假設我想日後用這個 function 來 define 不同類型的預設 type,甚至是一次 default 更多的 properties,這樣其實也是很不方便的。
最後回到了原點,重新構想了一下,想像如果可以在 define generic type 時直接把 default 的 object 也以 generic 的方式傳進去,概念像是這樣。
type ExtendsDefault<T, Default> = ... ;
type People<T> = ExtendsDefault<T, {
myId: string
}>
那就可以大大的提升可重用性和支援多個 default properties 了。
在翻了好一陣的 TypeScript 文檔後,發現了 Pick
混用 Exclude
可以將一個 object 的 properties「取代」另一個 object 的 properties:
Pick
用來從 Default
中選擇需要的 key。Exclude
用來從 Default
中排除已經存在於 T
中的 key。Partial
用來將 Default
中剩下的 key 轉換為 optional。type ExtendsDefault<T, Default> = T & Partial<Pick<Default, Exclude<keyof Default, keyof T>>>;
type People<T> = ExtendsDefault<T, {
myId: string
}>