简体   繁体   English

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

[英]Typescript union type for keys, infer type for dictionary values

I am trying to write a type which expresses the below without needing to explicitly create a type for the translations dictionary.我正在尝试编写一种表达以下内容的类型,而无需为翻译字典显式创建类型。

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

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

I have come up with this我想出了这个

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

but this doesn't quite satisfy my requirements, as T still needs to be passed to the type parameter...但这并不能完全满足我的要求,因为T仍然需要传递给类型参数......

TypeScript cannot express Translations as a specific type. TypeScript 无法将Translations表示为特定类型。 What you'd need is a different sort of generic type that most languages with generics doesn't have: a so-called existentially quantified generic type , or sometimes just existential type .您需要的是大多数具有 generics 的语言所没有的不同类型的泛型类型:所谓的存在量化泛型类型,或者有时只是存在类型 In your Translations type, you have a type parameter T ... but you don't want to have to specify it.在您的Translations类型中,您有一个类型参数T ... 但您不想指定它。 Instead, you want to say "the person who supplies a Translations value should be allowed to specify T to whatever they want; all I care or know about is that such a T exists ."相反,您想说“应该允许提供Translations值的人将T指定为他们想要的任何内容;我关心或知道的只是这样的T存在。” Maybe such a type definition would look like this:也许这样的类型定义如下所示:

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

But of course, you can't do this.但当然,你不能这样做。 There is no exists keyword in TypeScript and no direct method to quantify generic type parameters this way. TypeScript 中没有exists关键字,也没有直接的方法来量化泛型类型参数。 There's a feature request at microsoft/TypeScript#14466 for existential types, but it's not clear if that feature will ever be implemented.microsoft/TypeScript#14466有一个针对存在类型的功能请求,但尚不清楚该功能是否会实现。

There are ways to emulate existential types in TypeScript, but they are a bit convoluted and strange to use.在 TypeScript 中有一些方法可以模拟存在类型,但它们使用起来有点复杂和奇怪。 Instead of trying to do this, let's give up on existential types and instead look at possible workarounds.与其尝试这样做,不如让我们放弃存在类型,转而寻找可能的解决方法。


The most straightforward workaround is to define Translations<T> as a generic constraint on the type, with a type parameter T :最直接的解决方法是将Translations<T>定义为类型的通用约束,并带有类型参数T

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

To avoid requiring the user to specify T in a type annotation, you can make a helper function:为了避免要求用户在类型注释中指定T ,您可以创建一个帮助器 function:

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

This identity function just returns its input, so at runtime it's essentially a no-op.这个身份 function只是返回它的输入,所以在运行时它本质上是一个空操作。 But by constraining the input to Translations<T> , we are asking the compiler to infer the generic type T from the input.但是通过限制Translations<T>的输入,我们要求编译器从输入中推断出泛型类型T If the call compiles with no errors, then we know that the input is valid:如果调用编译没有错误,那么我们知道输入是有效的:

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

Here the compiler has inferred that T is {foo: string, bar: string} and therefore translations is inferred to be of type Translations<{foo: string, bar: string}> .这里编译器推断T{foo: string, bar: string} ,因此translations被推断为Translations<{foo: string, bar: string}>类型。

If you make a mistake, the compiler will warn you about it:如果你犯了错误,编译器会警告你:

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

In this case too, the compiler infers {foo: string; bar: string}在这种情况下,编译器也会推断出{foo: string; bar: string} {foo: string; bar: string} for T . {foo: string; bar: string}T But this time, there is an excess property warning on the baz property of german , because baz is neither foo nor bar .但这一次, german人的baz属性出现了多余的属性警告,因为baz既不是foo也不是bar

(Note that excess property warnings only happen with object literals. If you need to copy your object literal to a variable before calling asTranslations() on it, you won't get the warning. If that use case is important, you can switch to a different definition for Translations<T> , but I won't bother going into that here unless there's some need for it. It's in the linked Playground Code example below anyway.) (请注意,多余的属性警告仅发生在 object 文字上。如果您需要在调用asTranslations()之前将 object 文字复制到变量中,您将不会收到警告。如果该用例很重要,您可以切换到Translations<T>有一个不同的定义,但除非有需要,否则我不会在这里讨论它。无论如何,它都在下面链接的 Playground Code 示例中。)


One side effect of this workaround is that you will be required to make anything that deals with a Translations<T> type generic itself.此解决方法的一个副作用是,您将需要进行任何处理Translations<T>类型的泛型本身。 You can plausibly free up end users from having to specify T , but your code base will still end up dragging around an extra type parameter.您可以合理地使最终用户不必指定T ,但您的代码库最终仍将拖累额外的类型参数。 For example, a function which ideally would look like function saveTranslations(translations: Translations, filename: string): void {} will now look like function saveTranslations<T extends Record<keyof T, string>>(translations: Translations<T>, filename: string): void {} . For example, a function which ideally would look like function saveTranslations(translations: Translations, filename: string): void {} will now look like function saveTranslations<T extends Record<keyof T, string>>(translations: Translations<T>, filename: string): void {}

For that reason, it might be best to only require such a generic type in a validation function that accepts the input, and then in your internal library code you widen the type to something less accurate but easier to use:出于这个原因,最好只在接受输入的验证 function 中需要这样的泛型类型,然后在您的内部库代码中将类型扩展为不太准确但更易于使用的类型:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM