【问题标题】:Typescript string dot notation of nested object嵌套对象的打字稿字符串点表示法
【发布时间】:2022-03-12 00:57:05
【问题描述】:

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

viewName: {
    componentName: {
        title: 'translated title'
    }
}

我使用一个接受点符号字符串的翻译库来获取字符串,例如translate('viewName.componentName.title')

有什么方法可以强制 translate 的输入参数跟随 typescript 对象的形状?

我可以通过这样做来完成第一级:

translate(id: keyof typeof languageObject) {
    return translate(id)
}

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

【问题讨论】:

    标签: typescript


    【解决方案1】:

    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&lt;T&gt; 类。您通过给它一个对象和该对象的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);
      }
    }
        
    

    使用languageObjecttranslate() 函数对其进行初始化:

    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();
    

    希望对您有所帮助。祝你好运!

    【讨论】:

    • 令人印象深刻的解决方案!即使语法变得有点复杂,它也能胜任。
    • 这完全取决于您要查找的内容;即使在递归树状对象中,所有属性的所有路径?排除具有非点属性的路径?在不同问题的评论部分很难弄清楚这一点。您可能想要this 之类的东西,但我不确定。或者您可能只想问一个新问题。祝你好运!
    • @BjørnEgil 谢谢;我修改了上面的代码以使用Extract&lt;R, string[]&gt;。貌似4.1的release版本也需要这个。
    • 当我第一次工作时,(当 TS4.1 仍在开发中时)我认为没有像 `${string}.${string}` 这样的类型(即在 microsoft/TypeScript#40598 之前),所以 Join&lt;[string, string], '.'&gt; 会有无论如何只产生了string...我的代码提前退出而不是递归以获得相同的结果。可以按照您的建议进行更改。
    • @user5480949 可能喜欢this?旧答案的评论部分不是获得后续答案的理想场所,因此如果您仍有问题,您可能需要自己发帖。
    【解决方案2】:

    @jcalz 的回答很棒。

    如果你想添加其他类型如number | Date:

    你应该替换

    type PathsToStringProps<T> = T extends string ? [] : {
        [K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K]>]
    }[Extract<keyof T, string>];
    

    type PathsToStringProps<T> = T extends (string | number | Date) ? [] : {
        [K in keyof T]: [K, ...PathsToStringProps<T[K]>]
    }[keyof T];
    

    【讨论】:

      【解决方案3】:

      如果它的目标是提供自动完成,我能想到的唯一方法是创建一个类型来限制允许的字符串:

      type LanguageMap = 'viewName.componentName.title' | 'viewName.componentName.hint';
      
      function translate(id: LanguageMap) {
          return translate(id)
      }
      

      你不能使用你的 keyof 技巧自动生成这个,因为嵌套会阻止它。

      另一种方法是删除嵌套,在这种情况下,您的 keyof 技巧会为您创建语言映射类型:

      let languageObject = {
          'viewName.componentName.title': 'translated title',
          'viewName.componentName.hint': 'translated hint'
      };
      
      function translate(id: keyof typeof languageObject) {
          return translate(id)
      }
      

      但我不知道如何两全其美,因为一方面嵌套和键名之间存在逻辑中断。

      【讨论】:

      • 是的,我今天成功地使用了一个平面对象。嵌套翻译对象是我为改进翻译文件的结构而努力的目标。但是,列出所有可能的路径并不是一个合格的解决方案
      【解决方案4】:

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

      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>];
      

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

      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;
      };
      

      然后可以这样使用:

      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"
      ];
      

      【讨论】:

        猜你喜欢
        • 2021-06-28
        • 2022-07-31
        • 2021-09-25
        • 2020-02-14
        • 1970-01-01
        • 2020-11-30
        相关资源
        最近更新 更多