如何基于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
相关产品推荐
相关产品推荐

