【问题标题】:Typescript union type for keys, infer type for dictionary values键的打字稿联合类型,字典值的推断类型
【发布时间】:2021-05-09 11:16:27
【问题描述】:

我正在尝试编写一种表达以下内容的类型,而无需为翻译字典显式创建类型。

type Language = "english" | "german" | "french";

const translations = {
  english: {
    foo: "foo",
    bar: "bar"
  },
  german: {
    foo: "föö"
  }
};

我想出了这个

type Translations = {english: T, } & {[key in Language]?: Partial<T>};

但这并不能完全满足我的要求,因为T 仍然需要传递给类型参数...

【问题讨论】:

  • enum a 在里面做什么?
  • 还有...你想用你的Translations 类型做什么? TypeScript 中没有代表您预期类型的​​特定类型,这将需要存在量化的泛型类型参数,这是 TS 中不存在的功能(请求microsoft/TypeScript#14466)。所以我们所能做的就是解决它,为此,我想看看一些用例。
  • this 解决方法对您有用吗?这里我们在T 中保留Translations&lt;T&gt; 泛型,并使用辅助函数,这样您就不必手动传入T。如果是这样,请告诉我,我会写一个答案。如果没有,请考虑展示一些不适合您使用的代码示例。
  • 效果很好。我的用例是用于 react 项目的 sn-p:pastebin.com/XcNmaEiE 这已经有效,但我没有对翻译对象进行类型检查。

标签: typescript typescript-typings typescript-generics


【解决方案1】:

TypeScript 无法将 Translations 表示为特定类型。您需要的是大多数具有泛型的语言所没有的不同类型的泛型类型:所谓的存在量化泛型类型,或者有时只是存在类型 .在您的 Translations 类型中,您有一个类型参数 T... 但您不想指定它。相反,您想说“应该允许提供Translations 值的人将T 指定为他们想要的任何东西;我关心或知道的只是这样的T 存在 。”也许这样的类型定义应该是这样的:

/* NOT SUPPORTED
type Translations = <exists T extends Record<keyof T, string>>(
  { english: T } & { [K in Language]?: Partial<T> }
);
*/

当然,你不能这样做。 TypeScript 中没有 exists 关键字,也没有直接的方法来以这种方式量化泛型类型参数。 microsoft/TypeScript#14466 有一个针对存在类型的功能请求,但尚不清楚该功能是否会实现。

有一些方法可以在 TypeScript 中模拟存在类型,但它们使用起来有点复杂和奇怪。与其尝试这样做,不如让我们放弃存在类型,转而寻找可能的解决方法。


最直接的解决方法是将Translations&lt;T&gt; 定义为类型上的通用约束,并带有类型参数T

type Translations<T extends Record<keyof T, string>> = 
  { english: T } & { [K in Language]?: Partial<T> };

为避免要求用户在类型注释中指定T,您可以创建一个辅助函数:

const asTranslations = <T extends Record<keyof T, string>>(
  translations: Translations<T>) => translations;

这个身份函数只是返回它的输入,所以在运行时它本质上是一个空操作。但是通过将输入限制为Translations&lt;T&gt;,我们要求编译器从输入中推断泛型类型T。如果调用编译没有错误,那么我们知道输入是有效的:

const translations = asTranslations({
    english: {
        foo: "foo",
        bar: "bar"
    },
    german: {
        foo: "föö"
    }
}); // okay
/* const translations: Translations<{
  foo: string;
  bar: string;
}> */

这里编译器推断T{foo: string, bar: string},因此translations 被推断为Translations&lt;{foo: string, bar: string}&gt; 类型。

如果你犯了错误,编译器会警告你:

const badTranslations = asTranslations({
    english: {
        foo: "foo",
        bar: "bar"
    },
    german: {
        foo: "föö",
        baz: "baß" // error!
    //  ~~~~~~~~~~
    // Type '{ foo: string; baz: string; }' is not assignable 
    //    to type Partial<{ foo: string; bar: string; }>' 
    // Object literal may only specify known properties, and 'baz' 
    //    does not exist in type 'Partial<{ foo: string; bar: string; }>'
    }
});

在这种情况下,编译器也会将{foo: string; bar: string} 推断为T。但是这一次,germanbaz 属性上有一个excess property warning,因为baz 既不是foo 也不是bar

(请注意,多余的属性警告仅发生在对象文字上。如果您需要在调用 asTranslations() 之前将对象文字复制到变量中,您将不会收到警告。如果该用例很重要,您可以为Translations&lt;T&gt; 切换到不同的定义,但除非有需要,否则我不会在此讨论。无论如何,它都在下面链接的 Playground 代码示例中。)


此解决方法的一个副作用是,您将需要进行任何处理 Translations&lt;T&gt; 类型泛型本身的事情。您可以合理地使最终用户不必指定T,但您的代码库最终仍将拖累额外的类型参数。例如,理想情况下看起来像 function saveTranslations(translations: Translations, filename: string): void {} 的函数现在看起来像 function saveTranslations&lt;T extends Record&lt;keyof T, string&gt;&gt;(translations: Translations&lt;T&gt;, filename: string): void {}

因此,最好只在接受输入的验证函数中要求这样的泛型类型,然后在内部库代码中将类型扩展为不太准确但更易于使用的类型:

function userFacingFunction<T extends Record<keyof T, string>>(
  translations: Translations<T>) {
    internalLibraryFunction(translations);
}

type AnyTranslations = Translations<Record<string, string>>;
// not generic anymore
function internalLibraryFunction(translations: AnyTranslations) {
    // do something 
}

Playground link to code

【讨论】:

    猜你喜欢
    • 2022-01-05
    • 1970-01-01
    • 2021-06-08
    • 2018-08-12
    • 1970-01-01
    • 2018-06-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多