TS 自定義泛型工具例子-修改使定義的部分屬性必須存在

工作中常常用 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 的類型,思路是這樣的:

  1. Pick<TestOptions, 'num' | 'str'> 從類型中選取 numstr 兩個屬性,假設新類型取名叫 type A = { num?: number; str?: string }
  2. 加上 Required<...> 使得 A 的屬性全都要求必須存在,假設新類型取名叫 type B = { num: string; str: string }
  3. Omit<TestOptions, 'num' | 'str'> 使從類型中排除 numstr,假設新類型取名叫 type C = { hookFn?: () => string }
  4. 將 B 和 C 兩類型連結起來得到新類型,新類型裏 numstr 要求必須存在,而其他的屬性依照原類型定義不做改變。假設新類型取名叫 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
})

參考

Ts高手篇:22個示例深入講解Ts最晦澀難懂的高級類型工具

TS 一些工具泛型的使用及其實現

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章