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

如何基于TypeScript类型生成带默认值的对象而无需逐个设置?

实现TypeScript类型与默认值的统一管理

现有类型定义

你已经实现了一套通过类型标记默认值的方案,核心类型如下:

// 标记属性默认值的类型容器
type Default<Q> = [Q];

// 从类型中提取所有带默认值的属性,生成默认值类型
type Defaults<G> = Extract<G, object> extends never
  ? undefined
  : {
      // 收集不带默认值的属性,设为undefined
      [K in keyof G as Extract<G[K], Default<G[K]>> extends never
        ? K
        : never]: Defaults<Exclude<G[K], any[]>>;
    } & {
      // 收集带默认值的属性,提取实际默认值类型
      [K in keyof G as Extract<G[K], Default<G[K]>> extends never
        ? never
        : K]: Extract<G[K], Default<G[K]>> extends [infer A] ? A : never;
    } extends infer G
  ? { [K in keyof G]?: G[K] }
  : never;

// 从类型中移除Default<Q>标记,生成纯净的业务类型
type NonDefaults<F> = Extract<F, object> extends never
  ? F
  : { [K in keyof F]: NonDefaults<Exclude<F[K], Default<F[K]>>> };

使用示例

type Test = {
  id: string;
  way?: string | string[] | Default<"up">;
  isUp?: boolean | Default<true>;
  deep?: {
    hello: string;
    world?: string | Default<"#world">;
    deeper: {
      make: boolean | Default<true>;
    };
  };
};

// 提取出的默认值类型
type TestDefaults = Defaults<Test>;
// 纯净的业务类型(移除了Default标记)
type TestTypes = NonDefaults<Test>;

核心问题与需求

你需要实现mergeDefaults函数,自动生成符合TestDefaults类型的实际默认值对象,而非仅带类型标记的空对象,同时达成:

  • 避免手动重复创建默认值对象
  • 将类型定义、默认值、文档统一管理在一处

解决方案

TypeScript类型在编译时会被擦除,无法直接从类型生成运行时值,因此需要结合运行时工具函数与类型系统实现需求:

1. 连接类型与运行时的工具函数

实现一个withDefault函数,既标记类型默认值,又保留运行时值:

// 类型层面:标记默认值
type Default<Q> = [Q];

// 运行时工具:生成带默认值标记的对象,同时保留运行时值
function withDefault<Q>(value: Q): Default<Q> {
  return [value];
}

2. 实现默认值提取函数

编写递归函数,遍历配置模板,提取所有withDefault标记的默认值:

// 递归提取默认值的工具函数
function extractDefaults<T>(obj: T): Defaults<T> {
  const result: Partial<Defaults<T>> = {};

  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      const value = obj[key];
      // 判断是否是withDefault生成的默认值标记(数组且长度为1)
      if (Array.isArray(value) && value.length === 1) {
        result[key as keyof Defaults<T>] = value[0] as any;
      }
      // 如果是对象,递归提取默认值
      else if (typeof value === 'object' && value !== null) {
        result[key as keyof Defaults<T>] = extractDefaults(value) as any;
      }
    }
  }

  return result as Defaults<T>;
}

3. 统一管理类型与默认值

定义一份默认值模板,通过类型推导生成业务类型,避免重复定义:

// 定义默认值模板实例(仅需创建一次)
const testDefaultsTemplate = {
  way: withDefault("up"),
  isUp: withDefault(true),
  deep: {
    world: withDefault("#world"),
    deeper: {
      make: withDefault(true),
    },
  },
} as const;

// 从模板自动推导业务类型
type Test = {
  id: string;
} & {
  [K in keyof typeof testDefaultsTemplate]?: NonDefaults<(typeof testDefaultsTemplate)[K]> | (typeof testDefaultsTemplate)[K];
};

// 自动推导默认值类型
type TestDefaults = Defaults<typeof testDefaultsTemplate>;
// 自动推导纯净业务类型
type TestTypes = NonDefaults<Test>;

4. 重构mergeDefaults函数

现在可以自动生成默认值对象并与传入配置合并:

function mergeDefaults(config: Test): TestTypes {
  // 提取实际默认值对象
  const defaults = extractDefaults(testDefaultsTemplate);
  // 深合并配置与默认值(可根据需求替换为浅合并)
  const merged = { ...defaults, ...config };
  if (merged.deep) {
    merged.deep = { ...defaults.deep, ...merged.deep };
    if (merged.deep.deeper) {
      merged.deep.deeper = { ...defaults.deep?.deeper, ...merged.deep.deeper };
    }
  }
  return merged as TestTypes;
}

function useTest(config: Test) {
  config = mergeDefaults(config);
  // 示例:传入{ id: "123" },会自动补充所有默认值
}

关键优势

  • 一处定义默认值模板,类型与运行时值自动推导,避免重复维护
  • 类型系统确保默认值与业务类型的一致性
  • 仅需创建一次默认值模板,减少冗余对象创建

内容的提问来源于stack exchange,提问作者eslam sharif

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.21 18:48:24