網站搜尋

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";

但是,我也想確保該物件的值實現以下介面:

interface CountryDetails {
  name: string;
  continent: string;
  language: string;
}

我找到的唯一方法來同時達成兩者,是使用一個輔助函數

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;

現在 ISOCode 相當於 "DEU" | "GBR",而 iso3ToCountry 中的每個值都必須有一個 name、continent 和 language。

缺點是 ensureType 函數會出現在編譯後的 JavaScript 中。我們在應用程式中添加代碼,只是為了解決一些編譯時的問題。

問題:

我可以在「iso3ToCountry」對象中實現類型檢查,並且能夠推導鍵類型,而不必添加會保留在編譯後 JavaScript 中的代碼嗎。

答案#1

你真的想要所謂的 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

這裡的 CheckIso3ToCountry<T> 類型是一個虛擬類型,不論如何都會評估為 void。重要的部分是對 T 類型參數的泛型約束,強制它擴展 Record<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;

然後你會收到一個錯誤:

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'.

程式碼的 Playground 連結