我想编写一组 TypeScript 类型和一个函数,它将任意深度的对象作为输入,其中叶节点的形状为
{ text: string, [key: string]: string }
并返回一个具有相同整体结构的对象,但叶子是普通的 string
s.
例如
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"
}
返回的类型必须具有对象的特定键,因为我想使用它们来索引它。
我的最终用例是使用转换后的对象的键来调用函数,例如
getTranslation('button.continue')
。我已经有一个类型可以将对象键转换为点表示法形式,所以这里忽略它。我需要的是弄清楚如何映射任意深度的图状结构并变换叶节点。
我尝试过以下方法:
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;
但是,这会返回结果类型
{ [key: string]: never }
。值得注意的是,底层的实际 JS 按预期工作。我只需要键的类型安全。
你需要使用泛型类型,不能是静态类型。
您可以使用带有条件类型运算符的递归类型来仅转换与您的
Leaf
类型匹配的字段。
type FlattenedTranslationMap<T extends TranslationMap> = {
[K in keyof T]:
// check if it's a Leaf structure
// infer the text in case it is a constsnt and not just a generic string
T[K] extends {text: infer Text extends string}
// use inferred value
? Text
// check if it's a nested map
: T[K] extends TranslationMap
// recursively apply same transformations to it and it's children
? Simplified<FlattenedTranslationMap<T[K]>>
// if none of the abole leave untouched
: T[K];
}
type Output = FlattenedTranslationMap<Input>
/*
type Output = {
button: {
continue: string;
next: string;
};
error: string;
}
*/