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

Typescript union type for keys, infer type for dictionary values

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

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

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;

这个 身份函数 只是 returns 它的输入,所以在运行时它本质上是一个空操作。但是通过将输入限制为 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; }>'
    }
});

在这种情况下,编译器也会为 T 推断出 {foo: string; bar: string}。但是这次,germanbaz属性上多了一个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