You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 10:52:39