Open lmk123 opened 2 years ago
TypeScript 中有一个内置的工具类型 Partial,可以将类型的所有属性变成可选的:
Partial
const obj = Partial<{ test: number }> = { test: 1 } obj.test = undefined // OK
但是,这个工具类型只能将对象的第一层属性变成可选的,更深层次的就没作用了:
const obj = Partial<{ test: { deep: number } }> = { test: { deep: 1 } } obj.test.deep = undefined // Error
然而在实际项目中,大部分情况的对象都是更深层次的,所以我更希望能有一个 DeepPartial。
DeepPartial
我在以下两个项目中找到了 DeepPartial:
在尝试了这两个项目的 DeepPartial 之后,我发现它们都不符合我的需求,以致于最后我只能自己写了一个。
我希望对一个 JSON 对象进行深层次的读写,例如:
import pick from 'lodash/pick' import merge from 'lodash/merge' const json = { num: 1, str: 'string', strArr: ['a', 'b'] jsonArr: [{ id: 'string' }] } // 读取部分数据 function get(path: string): MyDeepPartial<typeof json> { return pick(json, path) } // 写入部分数据 function set(data: MyDeepPartial<typeof json>): void { merge(json, data) }
但是,我还希望数组内的类型不要被推断为 undefined,而这就是前面两个项目都不能满足我的地方。
undefined
因为我在写如下代码时,undefined 的存在会报错:
const map: { [id: string]: any } = { 'ID_FOR_STH': { a: '...', b: '...' } } get('jsonArr').jsonArr?.map(item => { // 由于 item.id 被 DeepPartial 加上了 undefined 类型, // 所以这里 TypeScript 会报错,说 undefined 不能应用于 string return map[item.id] })
type-fest 会给数组内的原始值和对象属性都加上 undefined:
import type { PartialDeep } from 'type-fest' const partialJSON: PartialDeep<typeof json> = {} partialJSON.strArr = [undefined] // TypeScript 没报错,但我希望它报错
ts-essentials 倒是不会给原始值组成的数组加上 undefined,但如果是对象组成的仍然会加:
import type { DeepPartial } from 'ts-essentials' const partialJSON: DeepPartial<typeof json> = {} partialJSON.strArr = [undefined] // TypeScript 报错了,这就是我希望的 partialJSON.jsonArr = [{ id: undefined }] // 我希望 id: undefined 会报错,但是 TypeScript 没有
没办法,看来只能自己写一个了。我首先看了 ts-essentials 的 DeepPartial 源码,看到这坨代码就头大:
https://github.com/ts-essentials/ts-essentials/blob/e0ded0bba5a18148dcbae9cc4306ac8173cb2e65/lib/types.ts#L32-L57
/** Like Partial but recursive */ export type DeepPartial<T> = T extends Builtin ? T : T extends Map<infer K, infer V> ? Map<DeepPartial<K>, DeepPartial<V>> : T extends ReadonlyMap<infer K, infer V> ? ReadonlyMap<DeepPartial<K>, DeepPartial<V>> : T extends WeakMap<infer K, infer V> ? WeakMap<DeepPartial<K>, DeepPartial<V>> : T extends Set<infer U> ? Set<DeepPartial<U>> : T extends ReadonlySet<infer U> ? ReadonlySet<DeepPartial<U>> : T extends WeakSet<infer U> ? WeakSet<DeepPartial<U>> : T extends Array<infer U> ? T extends IsTuple<T> ? { [K in keyof T]?: DeepPartial<T[K]> } : Array<DeepPartial<U> | undefined> : T extends Promise<infer U> ? Promise<DeepPartial<U>> : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> } : IsUnknown<T> extends true ? unknown : Partial<T>;
但是多看几遍就能找到关键点了:
T extends Array<infer U> ? ... : Array<DeepPartial<U> | undefined>
这段代码将数组内的类型 infer U 变成了 DeepPartial<U>,而当 U 是对象的时候,下面这段代码就将对象内的所有属性都变成了可选的:
infer U
DeepPartial<U>
T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
所以,改造的关键在于遇到数组类型的时候就原样返回。再考虑到我想要应用的类型只包含 JSON 允许的类型,所以最终我改造完毕的类型是这样的:
type JSONDeepPartialExcludeArray<T> = T extends | string | number | boolean | null | unknown[] ? T : T extends object ? { [K in keyof T]?: JSONDeepPartialExcludeArray<T[K]> } : Partial<T>
测试之后,完美满足了我的需求:
import pick from 'lodash/pick' import merge from 'lodash/merge' const json = { num: 1, str: 'string', strArr: ['a', 'b'] jsonArr: [{ id: 'string' }] } // 读取部分数据 function get(path: string): JSONDeepPartialExcludeArray<typeof json> { return pick(json, path) } const map: { [id: string]: any } = { 'ID_FOR_STH': { a: '...', b: '...' } } get('jsonArr').jsonArr?.map(item => { // item.id 被识别为仅为 string 类型,所以没有报错 return map[item.id] })
TypeScript 中有一个内置的工具类型
Partial
,可以将类型的所有属性变成可选的:但是,这个工具类型只能将对象的第一层属性变成可选的,更深层次的就没作用了:
然而在实际项目中,大部分情况的对象都是更深层次的,所以我更希望能有一个
DeepPartial
。我在以下两个项目中找到了
DeepPartial
:在尝试了这两个项目的
DeepPartial
之后,我发现它们都不符合我的需求,以致于最后我只能自己写了一个。先说说我的需求
我希望对一个 JSON 对象进行深层次的读写,例如:
但是,我还希望数组内的类型不要被推断为 undefined,而这就是前面两个项目都不能满足我的地方。
为什么我不希望数组内的类型被加上
undefined
?因为我在写如下代码时,
undefined
的存在会报错:为什么不满足?
type-fest 会给数组内的原始值和对象属性都加上 undefined:
ts-essentials 倒是不会给原始值组成的数组加上 undefined,但如果是对象组成的仍然会加:
自己动手写一个
没办法,看来只能自己写一个了。我首先看了 ts-essentials 的 DeepPartial 源码,看到这坨代码就头大:
https://github.com/ts-essentials/ts-essentials/blob/e0ded0bba5a18148dcbae9cc4306ac8173cb2e65/lib/types.ts#L32-L57
但是多看几遍就能找到关键点了:
这段代码将数组内的类型
infer U
变成了DeepPartial<U>
,而当 U 是对象的时候,下面这段代码就将对象内的所有属性都变成了可选的:所以,改造的关键在于遇到数组类型的时候就原样返回。再考虑到我想要应用的类型只包含 JSON 允许的类型,所以最终我改造完毕的类型是这样的:
测试之后,完美满足了我的需求: