具有匹配键的通用对象接口

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

我正在尝试创建一个通用接口来定义翻译字典,其中主键指定语言,并且在每种语言中都存在相同的键。

这是我迄今为止使用泛型的尝试(最好):

interface TranslationDict<K extends string, O extends K[number]> {
  eng: Record<K, string>;
  jpn: Record<O, string>;
}

const translationDict: TranslationDict<string, string> = {
  eng: {
    year: "year",
    month: "month",
    day: "day",
    file: "file",
  },
  jpn: {
    year: "年",
    month: "月",
    day: "日",
    file: "ファイル",
    file2: "bla", // I want this to throw a type error
  },
};

没有泛型,这可以工作,但似乎太多样板:

const cols = ["year", "month", "day", "file"] as const;

export const commonCols: {
  eng: Record<(typeof cols)[number], string>;
  jpn: Record<(typeof cols)[number], string>;
} = {
  eng: {
    year: "year",
    month: "month",
    day: "day",
    file: "file",
  },
  jpn: {
    year: "年",
    month: "月",
    day: "日",
    file: "ファイル",
    file2: "bla", // TYPE ERROR
  },
};

如何使用泛型来约束

jpn
的键与
eng
的键相同?

typescript typescript-generics
1个回答
0
投票

您确实想使用 generic 类型,例如

TranslationDict<K>
:

interface TranslationDict<K extends string> {
    eng: Record<K, string>,
    jpn: Record<K, string>
}    

但是当你注释类型时,你需要手动写出类型参数:

const dict: TranslationDict<"year" | "month" | "day" | "file"> = {
    eng: {
        year: "year",
        month: "month",
        day: "day",
        file: "file",
    },
    jpn: {
        year: "年",
        month: "月",
        day: "日",
        file: "ファイル",
        file2: "bla", // error!
        //~~~ <-- Object literal may only specify known properties
        // but 'file2' does not exist in type 
        // 'Record<"year" | "month" | "day" | "file", string>'
    },
};

这给了你你想要的 excess property 错误,但它是多余的,因为你被迫自己写

"year" | "month" | "day" | "file"
。你真的希望编译器从 eng
推断
,也许像

// don't write this, not valid
const dict: TranslationDict<infer> = { ⋯ };

但目前不支持。您无法推断泛型 type 的类型参数。 microsoft/TypeScript#32794 有一个功能请求,但在该功能实现之前,您需要另一种方法。


获得此类行为的最常见方法是编写一个通用的函数来提供帮助。当您调用泛型函数时,TypeScript 确实会推断泛型类型参数,因此我们可以使用该推断。辅助函数只是运行时的一个类似于

d => d
的标识。它的唯一目的是帮助推理。

像这样:

const translationDict = <K extends string>(
    d: TranslationDict<K>) => d;

然后调用该函数而不是注释:

const dict = translationDict({
    eng: { // error!
        year: "year",
        month: "month",
        day: "day",
        file: "file",
    },
    jpn: {
        year: "年",
        month: "月",
        day: "日",
        file: "ファイル",
        file2: "bla",
    },
});

这可行,但错误不在您想要的位置。编译器最终将

K
推断为
"year" | "month" | "day" | "file" | "file2"
,然后抱怨
eng
是错误的。也许这足以满足您的需求。

但是,如果您希望编译器仅从

K
推断
eng
,然后仅针对 K
check
jpn
,您可以使用
NoInfer
实用程序类型
来阻止从
 的话语进行推断
K
 属性中的 jpn
。即,将
TranslationDict<K>
更改为

interface TranslationDict<K extends string> {
    eng: Record<K, string>,
    jpn: Record<NoInfer<K>, string>
}    

一旦执行此操作,您最终将获得指定的所需行为:

const dict = translationDict({
    eng: {
        year: "year",
        month: "month",
        day: "day",
        file: "file",
    },
    jpn: {
        year: "年",
        month: "月",
        day: "日",
        file: "ファイル",
        file2: "bla", // error!
        //~~~ <-- Object literal may only specify known properties
        // but 'file2' does not exist in type 
        // 'Record<"year" | "month" | "day" | "file", string>'
    },
});

现在

K
被推断为
"year" | "month" | "day" | "file"
,因此对象字面量的
eng
属性很好,但是
jpn
有多余的
file2
属性。

Playground 代码链接

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