TS4.1 更新。字符串连接现在可以通过模板字符串类型在类型级别表示,在microsoft/TypeScript#40336 中实现。现在您可以获取一个对象并在类型系统中正确获取其虚线路径。
想象languageObject是这样的:
const languageObject = {
viewName: {
componentName: {
title: 'translated title'
}
},
anotherName: "thisString",
somethingElse: {
foo: { bar: { baz: 123, qux: "456" } }
}
}
首先,我们可以使用microsoft/TypeScript#40002 中实现的递归条件类型 和microsoft/TypeScript#39094 中实现的可变元组类型 将对象类型转换为元组的联合与其string-valued 属性对应的键数:
type PathsToStringProps<T> = T extends string ? [] : {
[K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K]>]
}[Extract<keyof T, string>];
然后我们可以使用模板字符串类型将字符串字面量元组连接到点分路径(或任何分隔符D:)
type Join<T extends string[], D extends string> =
T extends [] ? never :
T extends [infer F] ? F :
T extends [infer F, ...infer R] ?
F extends string ?
`${F}${D}${Join<Extract<R, string[]>, D>}` : never : string;
结合这些,我们得到:
type DottedLanguageObjectStringPaths = Join<PathsToStringProps<typeof languageObject>, ".">
/* type DottedLanguageObjectStringPaths = "anotherName" | "viewName.componentName.title" |
"somethingElse.foo.bar.qux" */
然后可以在translate()的签名中使用:
declare function translate(dottedString: DottedLanguageObjectStringPaths): string;
我们得到了我三年前所说的神奇行为:
translate('viewName.componentName.title'); // okay
translate('view.componentName.title'); // error
translate('viewName.component.title'); // error
translate('viewName.componentName'); // error
太棒了!
Playground link to code
TS4.1 之前的答案:
如果你想让 TypeScript 帮助你,你必须帮助 TypeScript。它对连接字符串文字的类型一无所知,所以这不起作用。我关于如何帮助 TypeScript 的建议可能比你想要的要多,但它确实会带来一些相当不错的类型安全保证:
首先,我假设你有一个知道它的languageObject 和一个translate() 函数(这意味着languageObject 可能被用来产生特定的translate() 函数)。 translate() 函数需要一个带点的字符串,表示嵌套属性的键列表,其中最后一个此类属性是 string-valued。
const languageObject = {
viewName: {
componentName: {
title: 'translated title'
}
}
}
// knows about languageObject somehow
declare function translate(dottedString: string): string;
translate('viewName.componentName.title'); // good
translate('view.componentName.title'); // bad first component
translate('viewName.component.title'); // bad second component
translate('viewName.componentName'); // bad, not a string
介绍Translator<T> 类。您通过给它一个对象和该对象的translate() 函数来创建一个,然后在链中调用它的get() 方法以深入了解键。 T 的当前值始终指向您通过get() 方法链选择的属性类型。最后,当您达到您关心的string 值时,您调用translate()。
class Translator<T> {
constructor(public object: T, public translator: (dottedString: string)=>string, public dottedString: string="") {}
get<K extends keyof T>(k: K): Translator<T[K]> {
const prefix = this.dottedString ? this.dottedString+"." : ""
return new Translator(this.object[k], this.translator, prefix+k);
}
// can only call translate() if T is a string
translate(this: Translator<string>): string {
if (typeof this.object !== 'string') {
throw new Error("You are translating something that isn't a string, silly");
}
// now we know that T is string
console.log("Calling translator on \"" + this.dottedString + "\"");
return this.translator(this.dottedString);
}
}
使用languageObject 和translate() 函数对其进行初始化:
const translator = new Translator(languageObject, translate);
并使用它。这可以根据需要工作:
const translatedTitle = translator.get("viewName").get("componentName").get("title").translate();
// logs: calling translate() on "viewName.componentName.title"
而且这些都会根据需要产生编译器错误:
const badFirstComponent = translator.get("view").get("componentName").get("title").translate();
const badSecondComponent = translator.get("viewName").get("component").get("title").translate();
const notAString = translator.get("viewName").translate();
希望对您有所帮助。祝你好运!