嵌套对象的打字稿字符串点表示法

Jak*_*kob 5 typescript

我有一个嵌套的翻译字符串对象,如下所示:

viewName: {
    componentName: {
        title: 'translated title'
    }
}
Run Code Online (Sandbox Code Playgroud)

我使用一个接受点表示法字符串的翻译库来获取字符串,就像这样translate('viewName.componentName.title')

有什么方法可以强制 translate 的输入参数遵循带有打字稿的对象的形状吗?

我可以通过这样做为第一级做到这一点:

translate(id: keyof typeof languageObject) {
    return translate(id)
}
Run Code Online (Sandbox Code Playgroud)

但我希望这种类型是嵌套的,这样我就可以像上面的例子一样确定我的翻译范围。

jca*_*alz 14

更新 TS4.1。字符串连接现在可以通过模板字符串类型在类型级别表示,在microsoft/TypeScript#40336 中实现。现在你可以在类型系统中获取一个对象并获得它的虚线路径。

想象一下languageObject是这样的:

const languageObject = {
    viewName: {
        componentName: {
            title: 'translated title'
        }
    },
    anotherName: "thisString",
    somethingElse: {
        foo: { bar: { baz: 123, qux: "456" } }
    }
}
Run Code Online (Sandbox Code Playgroud)

首先,我们可以使用microsoft/TypeScript#40002 中实现的递归条件类型microsoft/TypeScript#39094 中实现的可变元组类型将对象类型转换为与其值属性对应的键元组的联合:string

type PathsToStringProps<T> = T extends string ? [] : {
    [K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K]>]
}[Extract<keyof T, string>];
Run Code Online (Sandbox Code Playgroud)

然后我们可以使用模板字符串类型将字符串文字元组连接到虚线路径(或任何分隔符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;    
Run Code Online (Sandbox Code Playgroud)

结合这些,我们得到:

type DottedLanguageObjectStringPaths = Join<PathsToStringProps<typeof languageObject>, ".">
/* type DottedLanguageObjectStringPaths = "anotherName" | "viewName.componentName.title" | 
      "somethingElse.foo.bar.qux" */
Run Code Online (Sandbox Code Playgroud)

然后可以在签名中使用translate()

declare function translate(dottedString: DottedLanguageObjectStringPaths): string;
Run Code Online (Sandbox Code Playgroud)

我们得到了我三年前所说的神奇行为:

translate('viewName.componentName.title'); // okay
translate('view.componentName.title'); // error
translate('viewName.component.title'); // error
translate('viewName.componentName'); // error
Run Code Online (Sandbox Code Playgroud)

惊人的!

Playground 链接到代码


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
Run Code Online (Sandbox Code Playgroud)

介绍Translator<T>班级。您可以通过为其提供一个对象和该对象的translate()函数来创建一个,然后get()在链中调用其方法以深入了解键。的当前值T始终指向您通过get()方法链选择的属性类型。最后,translate()当你达到string你关心的价值时,你会打电话。

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);
  }
}
    
Run Code Online (Sandbox Code Playgroud)

用它languageObjecttranslate()函数初始化它:

const translator = new Translator(languageObject, translate);
Run Code Online (Sandbox Code Playgroud)

并使用它。这可以根据需要工作:

const translatedTitle = translator.get("viewName").get("componentName").get("title").translate();
// logs: calling translate() on "viewName.componentName.title"
Run Code Online (Sandbox Code Playgroud)

这些都会根据需要产生编译器错误:

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();
Run Code Online (Sandbox Code Playgroud)

希望有帮助。祝你好运!


小智 10

我做了一个替代解决方案:

type BreakDownObject<O, R = void> = {
  [K in keyof O as string]: K extends string
    ? R extends string
      ? ObjectDotNotation<O[K], `${R}.${K}`>
      : ObjectDotNotation<O[K], K>
    : never;
};

type ObjectDotNotation<O, R = void> = O extends string
  ? R extends string
    ? R
    : never
  : BreakDownObject<O, R>[keyof BreakDownObject<O, R>];
Run Code Online (Sandbox Code Playgroud)

可以轻松修改它以接受未完成的点符号字符串。在我的项目中,我们使用它来将翻译对象属性列入白名单/黑名单。

type BreakDownObject<O, R = void> = {
  [K in keyof O as string]: K extends string
    ? R extends string
      // Prefix with dot notation as well 
      ? `${R}.${K}` | ObjectDotNotation<O[K], `${R}.${K}`>
      : K | ObjectDotNotation<O[K], K>
    : never;
};
Run Code Online (Sandbox Code Playgroud)

然后可以像这样使用:

const TranslationObject = {
  viewName: {
    componentName: {
      title: "translated title"
    }
  }
};

// Original solution
const dotNotation: ObjectDotNotation<typeof TranslationObject> = "viewName.componentName.title"

// Modified solution
const dotNotations: ObjectDotNotation<typeof TranslationObject>[] = [
  "viewName",
  "viewName.componentName",
  "viewName.componentName.title"
];
Run Code Online (Sandbox Code Playgroud)