键的打字稿联合类型,字典值的推断类型

时间:2021-05-09 00:44:42

标签: typescript typescript-typings typescript-generics

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

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 仍然需要传递给类型参数...

1 个答案:

答案 0 :(得分: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<T> 定义为类型上的通用约束,并带有一个类型参数 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<T>,我们要求编译器从输入中推断泛型类型 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<{foo: string, bar: string}> 类型。

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

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。但是这一次,bazgerman 属性上有一个 excess property warning,因为 baz 既不是 foo 也不是 bar

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


此变通方法的一个副作用是您将需要制作任何处理 Translations<T> 类型泛型本身的内容。您可以合理地使最终用户不必指定 T,但您的代码库最终仍会拖累额外的类型参数。例如,理想情况下看起来像 function saveTranslations(translations: Translations, filename: string): void {} 的函数现在看起来像 function saveTranslations<T extends Record<keyof T, string>>(translations: Translations<T>, 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