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 一些工具泛型的使用及其实现

posted @ 2022-10-18 17:48  Ever-Lose  阅读(728)  评论(0编辑  收藏  举报