通过类型计算,为 lodash.get实现完善的出入参类型推断
前言
众所周知,滥用?.
符号会使项目的体积快速增长。点我看看体积是怎么爆炸的
使用 lodash.get工具能安全的取对应对象的值。但是使用_.get
无法拥有完整的类型分析,也就是说,咱们写的定义文件如果通过 get 方法取值,则无法享受到 type 类型福利。
能不能有一个既类型完善,又安全的 get方法呢?让咱们来造一个轮子。
效果
解读
- 针对传入的对象和 pathChain,能获取到对应的值。
- 基于传入的对象,能生成 pathChain,输入正确的 pathChain,能识别出 pathChain 指向对象的类型。
- 再也不用写
?.
来增加项目体积了。
代码实现
我们举例最简单的一种类型,即将对象取值的 pathChain 以.
连接,则只需要将传入的 pathChain使用 split 方法拆成数组,并通过循环遍历的方式取值。此处简单实现取值逻辑,着重演示下面类型实现。
类型实现
众所周知,可以用 typescript 的类型来计算,下面来用类型系统根据函数第一个入参 计算出第二个入参 ,和出参的类型。
Keyof
Keyof 用于接收对象后,获取对象/数组上面的值。
ts
// ts里面 extends 可以作为一个判断节点使用
// 例如 type a = 1 extends number ? true : false
// 此时 a === true
type Keyof<T> = T extends any[]
? number
: T extends Record<any, any>
? keyof T
: never;
Split
Split用于接收访问链路后,分解成数组,并进行后续计算。
ts
// infer 用于在递归中获取对应位置的值,例如下方的 infer F用于获取第一个点前面的值
// 如果获取到了,则在第一个问号处可以使用infer 推断出的值,相当于声明了一个变量
// 注意:通过 infer 获取到的值在冒号后面无法正常使用。
type Split<T extends string> = T extends `${infer F}.${infer Rest}`
// 此处有一个递归
? [F, ...Split<Rest>]
: [T];
Split<'1.2.3'> = ['1', '2', '3']
GetChain
GetChain 用于接收第一个参数对象后,生成对象上所有的访问链。
ts
type GetChain<
// 此处声明 T 是xxx 类型的子类型,K 声明在 T 后面,因此可以使用 T。
// 注意此处 K 使用了 = 符号,因此是一个默认值。
T extends Record<any, any> | Array<any> | undefined,
K extends Keyof<T> = Keyof<Required<T>>
> = T extends undefined
// never 在类型计算中有什么意义待我温习一下补充
? never
: K extends string | number
// 返回当前找到的 key,并往其value 进行递归
? `${K}` | `${K}.${GetChain<Required<T[K]>>}`
: "";
{}
{a?: {b?: number}}
'a'
PickValue
PickValue 用于传入对象、取值链后,取指定对象的类型。
ts
type PickValue<
O extends Record<any, any>,
Chain extends string[],
// Nul用于判断当前元素是否可能为 undefined
Nul extends boolean = false
// 注意这里的 Rest extends string[],在 infer 中使用 extends 进线类型判断
// 需要 ts 4.7 支持
> = Chain extends [infer Head, ...infer Rest extends string[]]
// 类型守卫,ts 有些时候无法正确推断出 Head 的类型
// 可能会有问题这里 Head 的类型为啥不放在 infer 使用 extends 语法进行判断,
// 这里的原因我忘了
? Head extends string
// 还是一个递归
? PickValue<
Required<O>[Head],
Rest,
// 将当前元素是否可能为 undefined 进线透传+判断
// 如果前一个元素可能为 undefined,则当前元素也可能为 undefined
Nul extends true ? true : undefined extends O[Head] ? true : false
>
: O
: Nul extends true
? O | undefined
: O;
PickVlaue<{a: number}, 'a'> = number
函数声明
函数入参、出参类型
ts
// T是第一个入参的类型,P 是第二个入参的类型。
// 如果不在调用函数的时候显式传入类型,ts 会从参数中自动推断。
function _get<T extends any[] | Record<any, any> | null | undefined, P extends GetChain<T>>(
o: T,
pathChain: P
): PickValue<T, Split<P>> {
const path = pathChain.split(".");
let result = o;
try {
for (let i = 0; i < path.length; ++i) {
result = result[path[i]];
}
} catch (e) {}
return result as any;
}