工作中常常用 API 的入參是非必填的,而實例的屬性因爲有默認值而一定存在的情況,舉個例子:
type TestOptions = {
num?: number
str?: string
hookFn?: () => string
}
const defaultOptions = {
num: 1,
str: 'test'
}
Class Test {
options: TestOptions
constructor (options: TestOptions) {
this.options = Object.assign({}, defaultOptions, options)
}
excute () {
this.options.num // error: 類型“boolean | undefined”的參數不能賦給類型“boolean”的參數。
}
}
上述代碼中,我們先忽略 options
有開發者主動傳入 { num: undefined }
的情況。
實際上我們期望的是被 defaultOptions
輔助設置過後, this.options.num
一定會存在,而不用每次都得加以判斷。至於鉤子函數 this.options.hookFn
我們的確希望它只能被上層開發者傳入而存在。
爲此我們需要改一下 this.options
的類型,思路是這樣的:
Pick<TestOptions, 'num' | 'str'>
從類型中選取num
和str
兩個屬性,假設新類型取名叫type A = { num?: number; str?: string }
- 加上
Required<...>
使得 A 的屬性全都要求必須存在,假設新類型取名叫type B = { num: string; str: string }
Omit<TestOptions, 'num' | 'str'>
使從類型中排除num
和str
,假設新類型取名叫type C = { hookFn?: () => string }
- 將 B 和 C 兩類型連結起來得到新類型,新類型裏
num
和str
要求必須存在,而其他的屬性依照原類型定義不做改變。假設新類型取名叫type D = { num: string; str: string; hookFn?: () => string }
class Test {
options: Required<Pick<TestOptions, 'num' | 'str'>> & Omit<TestOptions, 'num' | 'str'>
}
這麼寫過於囉嗦,所以用一個自定義的泛型工具替代:
export type PickForRequired<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>
class Test {
options: PickForRequired<TestOptions, 'num' | 'str'>
excute () {
// this.options.
// num
// str
// hookFn?
}
}
最後完善下 this.options
賦值的邏輯,去避開 options
中開發者主動傳入某屬性爲 undefined 的情況,借用 lodash 函數去合併對象:
this.options = _.assignWith({}, defaultOptions, options, function (objectVal, sourceVal) {
return _.isUndefined(sourceVal) ? objectVal : sourceVal
})