如何用TypeScript定义泛型条件映射类型实现对象叶节点转换?
TypeScript 深度对象叶子节点转换实现
需求说明
需要编写TypeScript类型与函数,实现以下功能:
- 输入:任意深度的对象,其叶子节点结构为
{ text: string, [key: string]: string } - 输出:结构完全一致,但叶子节点替换为普通
string类型的对象(取叶子节点的text属性值) - 要求返回类型必须保留对象的具体键,支持通过键索引访问,最终用于生成点标记形式的键(如
getTranslation('button.continue'))
示例
输入:
const input = { button: { continue: { text: "Continue", isTranslated: false }, next: { text: "Proceed", context: "Button for next screen control" } }, error: { text: "Unknown error", context: "Err message when specific error cause is not known" } };
输出:
const output = { button: { continue: "Continue", next: "Proceed" }, error: "Unknown error" }
问题现状
自行编写的代码逻辑在JavaScript层面运行正常,但返回类型被推断为{ [key: string]: never },无法保留具体键的类型信息,需要修正类型定义以实现类型安全。
尝试的代码
type Graphlike<Leaf> = { [key: string]: Graphlike<Leaf> | Leaf; }; type Flattened<T, Leaf, FlattenedValue> = { [K in keyof T]: T[K] extends Leaf ? FlattenedValue : T[K] extends T ? Flattened<T, Leaf, FlattenedValue> : never; }; type TranslationLeaf = { text: string; [key: string]: unknown; }; type TranslationMap = Graphlike<TranslationLeaf>; type FlattenedTranslationMap = Flattened<TranslationMap, TranslationLeaf, string>; function isTranslationLeaf(obj: TranslationLeaf | TranslationMap): obj is TranslationLeaf { return "text" in obj; } export function simplifyTranslations<T extends TranslationMap>(obj: T): FlattenedTranslationMap { const result = {} as FlattenedTranslationMap; for (const key in obj) { if (isTranslationLeaf(obj[key])) { const node = obj[key] as TranslationLeaf; // @ts-ignore result[key] = node.text; } else { const node = obj[key] as TranslationMap; // @ts-ignore result[key] = simplifyTranslations(node); } } return result; } const output = simplifyTranslations(input); type ResultantType = typeof output;
解决方案
问题出在Flattened类型的递归逻辑上,原类型错误地使用了整个T而非当前键对应的T[K]进行递归。重新定义递归类型即可解决:
修正后的类型定义
// 定义叶子节点类型 type TranslationLeaf = { text: string; [key: string]: string | boolean; // 匹配示例属性类型,也可使用unknown }; // 递归定义转换后的对象类型 type SimplifiedTranslationMap<T> = { [K in keyof T]: T[K] extends TranslationLeaf ? string : T[K] extends Record<string, any> ? SimplifiedTranslationMap<T[K]> : never; }; // 输入对象的约束类型 type TranslationMap = { [key: string]: TranslationMap | TranslationLeaf; };
修正后的函数实现
function isTranslationLeaf(obj: unknown): obj is TranslationLeaf { return typeof obj === 'object' && obj !== null && 'text' in obj && typeof (obj as TranslationLeaf).text === 'string'; } export function simplifyTranslations<T extends TranslationMap>(obj: T): SimplifiedTranslationMap<T> { const result = {} as SimplifiedTranslationMap<T>; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { const value = obj[key]; if (isTranslationLeaf(value)) { result[key] = value.text as SimplifiedTranslationMap<T>[Extract<keyof T, string>]; } else { result[key] = simplifyTranslations(value) as SimplifiedTranslationMap<T>[Extract<keyof T, string>]; } } } return result; }
验证效果
const input = { button: { continue: { text: "Continue", isTranslated: false }, next: { text: "Proceed", context: "Button for next screen control" } }, error: { text: "Unknown error", context: "Err message when specific error cause is not known" } }; const output = simplifyTranslations(input); // typeof output 会正确推断为: // { // button: { // continue: string; // next: string; // }; // error: string; // } // 支持键索引访问,无类型报错 output.button.continue;
关键说明
SimplifiedTranslationMap通过递归遍历每个键的类型:如果是TranslationLeaf则转为string,如果是对象则继续递归转换- 类型守卫
isTranslationLeaf做了更严格的校验,避免误判 - 函数中使用
Extract<keyof T, string>确保键的类型正确映射,移除了@ts-ignore注释
内容的提问来源于stack exchange,提问作者Stuart
相关产品推荐
相关产品推荐

