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。但是这一次,german 的baz 属性上有一个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