TypeScript中安全设置嵌套可选属性的简化方案
解决TypeScript嵌套可选类型深层字段赋值的冗余问题
针对嵌套可选类型的深层字段赋值,以下是几种类型安全且简洁的解决方案:
1. 修复递归泛型ensure函数
你之前的递归函数核心思路可行,但类型推导存在缺陷,未正确处理可选类型的非undefined分支。调整后的版本可实现链式调用并保证类型安全:
type NestedOptional = { a?: { b?: { c?: { d?: number; }; }; }; }; function ensure<T>(obj: T) { return function <K extends keyof T>( key: K, ctor: () => Exclude<T[K], undefined> ) { if (obj[key] == undefined) { obj[key] = ctor() as T[K]; } return ensure(obj[key] as Exclude<T[K], undefined>); }; } const emptyObj = () => ({}); function fillLeafValue(obj: NestedOptional, value: number) { ensure(obj)("a", emptyObj)("b", emptyObj)("c", emptyObj); obj.a!.b!.c!.d = value; // 此处!断言安全,因为ensure已保证层级存在 }
关键调整:
- 用
Exclude<T[K], undefined>明确构造函数需返回可选字段的非undefined类型,避免类型不匹配 - 返回的
ensure实例自动推导下一层级类型,确保后续链式调用的键名合法
2. 类型安全的元组路径赋值函数
若不想用链式调用,可实现基于元组路径的赋值函数,既规避字符串路径的不安全,又自动初始化中间层级:
function setDeep<T, Path extends PropertyKey[], V>( obj: T, path: Path, value: V, ctor: () => Record<string, any> = () => ({}) ): asserts obj is T & DeepRequiredByPath<T, Path> { let current: any = obj; for (let i = 0; i < path.length - 1; i++) { const key = path[i]; if (current[key] == undefined) { current[key] = ctor(); } current = current[key]; } current[path[path.length - 1]] = value; } // 辅助类型:确保路径上的层级都被初始化 type DeepRequiredByPath<T, Path extends PropertyKey[]> = Path extends [infer K extends keyof T, ...infer Rest extends PropertyKey[]] ? T extends Record<K, infer V> ? { [P in K]-?: DeepRequiredByPath<V, Rest> } & Omit<T, K> : T : T; // 使用示例 function fillLeafValue3(obj: NestedOptional, value: number) { setDeep(obj, ["a", "b", "c", "d"], value); // 传入非法路径如["a", "z", "c"]会直接触发编译报错 obj.a.b.c.d; // 此时字段类型不再可选,无需额外断言 }
核心优势:
- 元组路径
["a", "b", "c", "d"]会被TypeScript严格校验,非法键名直接拦截 asserts断言让TypeScript识别函数调用后路径层级已存在,后续访问类型安全
3. 结合Immer的简化方案
既然你在使用Immer,可在produce中集成上述工具函数,彻底消除冗余初始化代码:
import { produce } from "immer"; function setDeepDraft<Draft, Path extends PropertyKey[], V>( draft: Draft, path: Path, value: V, ctor: () => Record<string, any> = () => ({}) ) { let current: any = draft; for (let i = 0; i < path.length - 1; i++) { const key = path[i]; if (current[key] == undefined) { current[key] = ctor(); } current = current[key]; } current[path[path.length - 1]] = value; } // 使用示例 const updatedObj = produce<NestedOptional>({}, (draft) => { setDeepDraft(draft, ["a", "b", "c", "d"], 123); }); // updatedObj类型自动推导为{a: {b: {c: {d: number}}}}
适配Immer的特点:
- 直接在draft上操作,无需手动处理不可变数据
- 赋值后draft的类型会自动更新,后续访问无需额外处理
总结
- 偏好链式调用:选择修复后的
ensure函数,直观易读 - 需一次性赋值:使用元组路径的
setDeep函数,类型安全性最高 - 结合Immer开发:集成
setDeepDraft,彻底消除冗余初始化代码
内容的提问来源于stack exchange,提问作者Yann Vo
相关产品推荐
相关产品推荐

