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

问题描述 投票:0回答:4

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

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

我使用接受点表示法字符串的翻译库来获取字符串,就像这样

translate('viewName.componentName.title')

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

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

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

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

typescript
4个回答
54
投票

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
值属性:

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 代码链接


TS4.1 之前的答案:

如果你想让 TypeScript 帮助你,你就必须帮助 TypeScript。它不知道有关连接字符串文字的类型的任何信息,因此这是行不通的。我对如何帮助 TypeScript 的建议可能比您想要的要多,但它确实带来了一些相当不错的类型安全保证:


首先,我假设您有一个

languageObject
和一个了解它的
translate()
函数(这意味着
languageObject
可能用于生成特定的
translate()
函数)。
translate()
函数需要一个点分字符串,表示嵌套属性的键列表,其中最后一个此类属性是
string
值。

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()
方法链选择的属性类型。最后,当您达到您关心的
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);
  }
}
    

使用

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

希望有帮助。祝你好运!


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

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

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

4
投票

@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 Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K]>]
}[Extract<keyof T, string>];

0
投票

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

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

但我知道没有办法两全其美,因为一方面是嵌套,另一方面是键名称之间存在逻辑断裂。

© www.soinside.com 2019 - 2024. All rights reserved.