从类型化对象派生精确类型

Nil*_*ier 5 typescript

在 TypeScript 中,我想从这样的常量派生类型

const iso3ToCountry = {
  DEU: {
    name: "Germany",
    continent: "Europe",
    language: "de",
  },
  GBR: {
    name: "United Kingdom of Great Britain and Northern Ireland",
    continent: "Europe",
    language: "en",
  },
} as const;

type ISOCode = keyof typeof iso3ToCountry // same as "DEU" | "GBR";
Run Code Online (Sandbox Code Playgroud)

但是,我还想确保对象的值实现以下接口:

interface CountryDetails {
  name: string;
  continent: string;
  language: string;
}
Run Code Online (Sandbox Code Playgroud)

我发现实现这两个目标的唯一方法是使用辅助函数

function ensureType<T extends Record<string, CountryDetails>>(obj: T): T {
  return obj;
}

const iso3ToCountry = ensureType({
  DEU: {
    name: "Germany",
    continent: "Europe",
    language: "de",
  },
  GBR: {
    name: "United Kingdom",
    continent: "Europe",
    language: "en",
  },
} as const);

type ISOCode = keyof typeof iso3ToCountry;
Run Code Online (Sandbox Code Playgroud)

现在ISOCode相当于"DEU" | "GBR"iso3ToCountry 中的每个值都必须有一个name,continent和一个language

缺点是该ensureType函数将位于编译后的 JavaScript 中。我们向应用程序添加代码,只是为了解决一些编译时问题。

问题:

我可以在“iso3ToCountry”对象中实现类型检查以及派生键类型的能力,而无需添加保留在编译后的 JavaScript 中的代码吗?

jca*_*alz 5

您确实需要所谓的satisfies运算符,如microsoft/TypeScript#7481中所要求的那样,但这目前在 TypeScript 中不存在(截至 4.5)。您使用的通用辅助函数方法是一种非常常见的解决方法,特别是因为它几乎适用于任何表达式。发出的 JavaScript 中的额外函数调用通常不是什么大问题,但您正在寻找运行时影响为零的东西。


另一种解决方法是在与要检查的表达式类型相同的变量上使用typeof类型运算符来定义类型,编译器将成功或失败,具体取决于所需的约束。

您不能typeof在任意表达式上使用运算符;它必须是变量或变量的属性。因此,如果您需要声明新变量,此解决方法可能会对运行时产生一些影响。

由于您已经有一个名为iso3ToCountry正确类型的变量,因此不必定义新变量,因此不会对运行时产生影响。

这是一种方法:

type CheckIso3ToCountry<T extends Record<keyof T, CountryDetails> =
  typeof iso3ToCountry> = void; 
//^^^^^^^^^^^^^^^^^^^^ <-- you've got a problem with 
// iso3ToCountry if and only if there's an error here
Run Code Online (Sandbox Code Playgroud)

这里的CheckIso3ToCountry<T>类型是一个虚拟类型,void无论如何都会计算结果。重要的部分是强制类型参数扩展的通用约束以及 的通用参数默认值。只要后一种类型可以分配给前一种类型,一切都很好。TRecord<keyof T, CountryDetails>typeof iso3ToCountry

但如果你犯了一个错误iso3ToCountry

const iso3ToCountry = {
  DEU: {
    name: "Germany",
    continent: "Europe",
    language: "de",
  },
  GBR: {
    name: "United Kingdom of Great Britain and Northern Ireland",
    continant: "Europe",
    language: "en",
  },
} as const;
Run Code Online (Sandbox Code Playgroud)

然后你会得到一个错误:

type CheckIso3ToCountry<T extends Record<keyof T, CountryDetails> =
  typeof iso3ToCountry> = void; // error!
//~~~~~~~~~~~~~~~~~~~~ <--
// Property 'continent' is missing in type 
// '{ readonly name: "United Kingdom of Great Britain and Northern Ireland";
//  readonly continant: "Europe"; readonly language: "en"; }' 
// but required in type 'CountryDetails'.
Run Code Online (Sandbox Code Playgroud)

Playground 代码链接