如何在 TypeScript 中使用泛型

英文:

https://www.digitalocean.com/community/tutorials/how-to-use-generics-in-typescript

介紹

泛型是靜態類型語言的基本特徵,允許開發人員將類型作爲參數傳遞給另一種類型、函數或其他結構。當開發人員使他們的組件成爲通用組件時,他們使該組件能夠接受和強制在使用組件時傳入的類型,這提高了代碼靈活性,使組件可重用並消除重複。

TypeScript 完全支持泛型,以此將類型安全性引入到接受參數和返回值的組件中,這些參數和返回值的類型,在稍後的代碼中使用之前是不確定的。在今天的內容中,我們將嘗試 TypeScript 泛型的真實示例,並探索它們如何在函數、類型、類和接口中使用。

我們還將使用泛型創建映射類型和條件類型,這將幫助我們創建可以靈活應用於代碼中所有必要情況的 TypeScript 組件。

準備工作

介紹

TypeScript 是 JavaScript 語言的擴展,它使用 JavaScript 運行時和編譯時類型檢查器。

TypeScript 提供了多種方法來表示代碼中的對象,其中一種是使用接口。 TypeScript 中的接口有兩種使用場景:您可以創建類必須遵循的約定,例如,這些類必須實現的成員,還可以在應用程序中表示類型,就像普通的類型聲明一樣。 

您可能會注意到接口和類型共享一組相似的功能。

事實上,一個幾乎總是可以替代另一個。

主要區別在於接口可能對同一個接口有多個聲明,TypeScript 將合併這些聲明,而類型只能聲明一次。您還可以使用類型來創建原始類型(例如字符串和布爾值)的別名,這是接口無法做到的。

TypeScript 中的接口是表示類型結構的強大方法。它們允許您以類型安全的方式使用這些結構並同時記錄它們,從而直接改善開發人員體驗。

在今天的文章中,我們將在 TypeScript 中創建接口,學習如何使用它們,並瞭解普通類型和接口之間的區別。

我們將嘗試不同的代碼示例,可以在 TypeScript 環境或 TypeScript Playground(一個允許您直接在瀏覽器中編寫 TypeScript 的在線環境)中遵循這些示例。

準備工作

要完成今天的示例,我們將需要做如下準備工作:

本教程中顯示的所有示例都是使用 TypeScript 4.2.3 版創建的。

泛型語法

在進入泛型應用之前,本教程將首先介紹 TypeScript 泛型的語法,然後通過一個示例來說明它們的一般用途。

泛型出現在尖括號內的 TypeScript 代碼中,格式爲 ,其中 T 表示傳入的類型。 可以理解爲 T 類型的泛型。

在這種情況下,T 將以與函數中參數相同的方式運行,作爲將在創建結構實例時聲明的類型的佔位符。因此,尖括號內指定的泛型類型也稱爲泛型類型參數或只是類型參數。多個泛型類型也可以出現在單個定義中,例如 <T, K, A>。

注意:按照慣例,程序員通常使用單個字母來命名泛型類型。這不是語法規則,你可以像 TypeScript 中的任何其他類型一樣命名泛型,但這種約定有助於立即向那些閱讀你的代碼的人傳達泛型類型不需要特定類型。

泛型可以出現在函數、類型、類和接口中。本教程稍後將介紹這些結構中的每一個,但現在將使用一個函數作爲示例來說明泛型的基本語法。

要了解泛型有多麼有用,假設您有一個 JavaScript 函數,它接受兩個參數:一個對象和一個鍵數組。該函數將基於原始對象返回一個新對象,但僅包含您想要的鍵:

function pickObjectKeys(obj, keys) {
  let result = {}
  for (const key of keys) {
    if (key in obj) {
      result[key] = obj[key]
    }
  }
  return result
}

此代碼段顯示了 pickObjectKeys() 函數,該函數遍歷 keys 數組並使用數組中指定的鍵創建一個新對象。

下面是一個展示如何使用該函數的示例:

const language = {
  name: "TypeScript",
  age: 8,
  extensions: ['ts', 'tsx']
}
const ageAndExtensions = pickObjectKeys(language, ['age', 'extensions'])

這聲明瞭一種對象,然後使用 pickObjectKeys() 函數隔離 age 和 extensions 屬性。 ageAndExtensions 的值如下:

{
  age: 8,
  extensions: ['ts', 'tsx']
}

如果要將此代碼遷移到 TypeScript 以使其類型安全,則必須使用泛型。 我們可以通過添加以下突出顯示的行來重構代碼:

function pickObjectKeys<T, K extends keyof T>(obj: T, keys: K[]) {
  let result = {} as Pick<T, K>
  for (const key of keys) {
    if (key in obj) {
      result[key] = obj[key]
    }
  }
  return result
}
const language = {
  name: "TypeScript",
  age: 8,
  extensions: ['ts', 'tsx']
}
const ageAndExtensions = pickObjectKeys(language, ['age', 'extensions'])

<T, K extends keyof T> 爲函數聲明瞭兩個參數類型,其中 K 被分配一個類型,該類型是 T 中的 key 的並集。

然後將 obj 函數參數設置爲 T 表示的任何類型,並將 key 設置爲數組, 無論 K 代表什麼類型。 

由於在語言對象的情況下 T 將 age 設置爲數字並將 extensions 設置爲字符串數組,因此,變量 ageAndExtensions 現在將被分配具有屬性 age: number 和 extensions: string[] 的對象的類型。

這會根據提供給 pickObjectKeys 的參數強制執行返回類型,從而允許函數在知道需要強制執行的特定類型之前靈活地強制執行類型結構。 

當在 Visual Studio Code 等 IDE 中使用該函數時,這也增加了更好的開發人員體驗,它將根據您提供的對象爲 keys 參數創建建議。 這顯示在以下屏幕截圖中:

瞭解如何在 TypeScript 中創建泛型後,您現在可以繼續探索在特定情況下使用泛型。 本教程將首先介紹如何在函數中使用泛型。

將泛型與函數一起使用

將泛型與函數一起使用的最常見場景之一是當您有一些代碼不容易爲所有用例鍵入時。 爲了使該功能適用於更多情況,您可以包括泛型類型。 

在此步驟中,您將運行一個恆等函數示例來說明這一點。 您還將探索一個異步示例,瞭解何時將類型參數直接傳遞給您的泛型,以及如何爲您的泛型類型參數創建約束和默認值。

分配通用參數

看一下下面的函數,它返回作爲第一個參數傳入的內容:

function identity(value) {
  return value;
}

您可以添加以下代碼以使函數在 TypeScript 中類型安全:

function identity<T>(value: T): T{
  return value;
}

你把你的函數變成了一個泛型函數,它接受泛型類型參數 T,這是第一個參數的類型,然後將返回類型設置爲與 : T 相同。

接下來,添加以下代碼來試用該功能:

function identity<T>(value: T): T {
  return value;
}
const result = identity(123);

結果的類型爲 123,這是您傳入的確切數字。這裏的 TypeScript 從調用代碼本身推斷泛型類型。 這樣調用代碼不需要傳遞任何類型參數。 您也可以顯式地將泛型類型參數設置爲您想要的類型:

function identity<T>(value: T): T {
  return value;
}
const result = identity<number>(123);

在此代碼中,result 具有類型編號。 通過使用 代碼傳入類型,您明確地讓 TypeScript 知道您希望身份函數的泛型類型參數 T 的類型爲 number。 這將強制將數字類型作爲參數和返回值。

直接傳遞類型參數

直接傳遞類型參數在使用自定義類型時也很有用。 例如,看看下面的代碼:

type ProgrammingLanguage = {
  name: string;
};
function identity<T>(value: T): T {
  return value;
}
const result = identity<ProgrammingLanguage>({ name: "TypeScript" });

在此代碼中,result 具有自定義類型 ProgrammingLanguage,因爲它直接傳遞給標識函數。 如果您沒有明確包含類型參數,則結果將具有類型 {name: string} 。

使用 JavaScript 時的另一個常見示例是使用包裝函數從 API 檢索數據:

async function fetchApi(path: string) {
  const response = await fetch(`https://example.com/api${path}`)
  return response.json();
}

此異步函數將 URL 路徑作爲參數,使用 fetch API 向 URL 發出請求,然後返回 JSON 響應值。 在這種情況下,fetchApi 函數的返回類型將是 Promise,這是對 fetch 的響應對象調用 json() 的返回類型。

將 any 作爲返回類型並不是很有幫助。 any 表示任何 JavaScript 值,使用它你將失去靜態類型檢查,這是 TypeScript 的主要優點之一。 如果您知道 API 將返回給定形狀的對象,則可以使用泛型使此函數類型安全:

async function fetchApi<ResultType>(path: string): Promise<ResultType>{
  const response = await fetch(`https://example.com/api${path}`);
  return response.json();
}

突出顯示的代碼將您的函數轉換爲接受 ResultType 泛型類型參數的泛型函數。 此泛型類型用於函數的返回類型:Promise

注意:由於您的函數是異步的,因此,您必須返回一個 Promise 對象。 TypeScript Promise 類型本身是一種通用類型,它接受 promise 解析爲的值的類型。

如果仔細查看您的函數,您會發現參數列表或 TypeScript 能夠推斷其值的任何其他地方都沒有使用泛型。 這意味着調用代碼在調用您的函數時必須顯式傳遞此泛型的類型。

以下是檢索用戶數據的 fetchApi 通用函數的可能實現:

type User = {
  name: string;
}
async function fetchApi<ResultType>(path: string): Promise<ResultType> {
  const response = await fetch(`https://example.com/api${path}`);
  return response.json();
}
const data = await fetchApi<User[]>('/users')
export {}

在此代碼中,您將創建一個名爲 User 的新類型,並使用該類型的數組 (User[]) 作爲 ResultType 泛型參數的類型。 數據變量現在具有類型 User[] 而不是任何。

注意:當您使用 await 異步處理函數的結果時,返回類型將是 Promise 中 T 的類型,在本例中是通用類型 ResultType。

默認類型參數

像您一樣創建通用的 fetchApi 函數,調用代碼始終必須提供類型參數。 如果調用代碼不包含泛型類型,則 ResultType 將綁定爲未知。 以下面的實現爲例:

async function fetchApi<ResultType>(path: string): Promise<ResultType> {
  const response = await fetch(`https://example.com/api${path}`);
  return 
response.json();
}
const data = await fetchApi('/users')
console.log(data.a)
export {}

此代碼嘗試訪問數據的理論上的屬性。 但由於數據類型未知,這段代碼將無法訪問對象的屬性。

如果您不打算將特定類型添加到泛型函數的每次調用中,則可以將默認類型添加到泛型類型參數中。 這可以通過在泛型類型之後添加 = DefaultType 來完成,如下所示:

async function fetchApi<ResultType= Record<string, any>>(path: string): Promise<ResultType> {
  const response = await fetch(`https://example.com/api${path}`);
  return response.json();
}
const data = await fetchApi('/users')
console.log(data.a)
export {}

使用此代碼,您不再需要在調用 fetchApi 函數時將類型傳遞給 ResultType 泛型參數,因爲它具有默認類型 Record<string, any>。 這意味着 TypeScript 會將數據識別爲具有字符串類型的鍵和任意類型的值的對象,從而允許您訪問其屬性。

類型參數約束

在某些情況下,泛型類型參數需要只允許將某些形狀傳遞給泛型。 要爲您的泛型創建額外的特殊層,您可以對您的參數施加約束。

假設您有一個存儲限制,您只能存儲所有屬性都具有字符串值的對象。 爲此,您可以創建一個函數,它接受任何對象並返回另一個對象,該對象具有與原始對象相同的鍵,但所有值都轉換爲字符串。 這個函數將被稱爲 stringifyObjectKeyValues。

這個函數將是一個通用函數。 這樣,您就可以使生成的對象具有與原始對象相同的形狀。 該函數將如下所示:

function stringifyObjectKeyValues<T extends Record<string, any>>(obj: T) {
  return Object.keys(obj).reduce((acc, key) =>  ({
    ...acc,
    [key]: JSON.stringify(obj[key])
  }), {} as { [K in keyof T]: string })
}

在此代碼中,stringifyObjectKeyValues 使用 reduce 數組方法迭代原始鍵數組,將值字符串化並將它們添加到新數組中。

爲確保調用代碼始終將對象傳遞給您的函數,您在泛型類型 T 上使用類型約束,如以下突出顯示的代碼所示:

function stringifyObjectKeyValues<Textends Record<string, any>>(obj: T) {
  // ...
}

extends Record<string, any> 被稱爲泛型類型約束,它允許您指定您的泛型類型必須可分配給 extends 關鍵字之後的類型。 

在這種情況下,Record<string, any> 表示一個具有字符串類型的鍵和任意類型的值的對象。 您可以讓您的類型參數擴展任何有效的 TypeScript 類型。

在調用 reduce 時,reducer 函數的返回類型基於累加器的初始值。 {} as { [K in keyof T]: string } 代碼通過對空對象 {} 進行類型轉換,將累加器初始值的類型設置爲 { [K in keyof T]: string }。 

type {[K in keyof T]: string } 創建一個新類型,它具有與 T 相同的鍵,但所有值都設置爲字符串類型,這稱爲映射類型,本教程將在後面的部分中進一步探討。

以下代碼顯示了 stringifyObjectKeyValues 函數的實現:

function stringifyObjectKeyValues<T extends Record<string, any>>(obj: T) {
  return Object.keys(obj).reduce((acc, key) =>  ({
    ...acc,
    [key]: JSON.stringify(obj[key])
  }), {} as { [K in keyof T]: string })
}
const stringifiedValues = stringifyObjectKeyValues({ a: "1", b: 2, c: true, d: [1, 2, 3]})

變量 stringifiedValues 將具有以下類型:

{
  a: string;
  b: string;
  c: string;
  d: string;
}

這將確保返回值與函數的目的一致。

本節介紹了將泛型與函數一起使用的多種方法,包括直接分配類型參數以及爲參數形狀設置默認值和約束。 

接下來,您將通過一些示例來了解泛型如何使接口和類適用於更多情況。

將泛型與接口、類和類型一起使用

在 TypeScript 中創建接口和類時,使用泛型類型參數來設置結果對象的形狀會很有用。 

例如,一個類可能具有不同類型的屬性,具體取決於傳遞給構造函數的內容。 在本節中,您將瞭解在類和接口中聲明泛型類型參數的語法,並檢查 HTTP 應用程序中的常見用例。

通用接口和類

要創建通用接口,您可以在接口名稱之後添加類型參數列表:

interface MyInterface<T> {
  field: T
}

這聲明瞭一個接口,該接口具有一個屬性字段,其類型由傳遞給 T 的類型確定。

對於類,語法幾乎相同:

class MyClass<T> {
  field: T
  constructor(field: T) {
    this.field = field
  }
}

通用接口 / 類的一個常見用例是當您有一個字段,其類型取決於客戶端代碼如何使用接口 / 類時。 

假設您有一個 HttpApplication 類,用於處理對 API 的 HTTP 請求,並且某些上下文值將傳遞給每個請求處理程序。 這樣做的一種方法是:

class HttpApplication<Context> {
  context: Context
  constructor(context: Context) {
    this.context = context;
  }
  // ... implementation
  get(url: string, handler: (context: Context) => Promise<void>): this {
    // ... implementation
    return this;
  }
}

此類存儲一個上下文,其類型作爲 get 方法中處理函數的參數類型傳入。 在使用過程中,傳遞給 get 處理程序的參數類型將從傳遞給類構造函數的內容中正確推斷出來。

...
const context = { someValue: true };
const app = new HttpApplication(context);
app.get('/api', async () => {
  console.log(context.someValue)
});

在此實現中,TypeScript 會將 context.someValue 的類型推斷爲布爾值。

通用類型

現在已經瞭解了類和接口中泛型的一些示例,您現在可以繼續創建泛型自定義類型。 將泛型應用於類型的語法類似於將泛型應用於接口和類的語法。 看看下面的代碼:

type MyIdentityType<T> = T

此泛型類型返回作爲類型參數傳遞的類型。 假設您使用以下代碼實現了這種類型:

...
type B = MyIdentityType<number>

在這種情況下,類型 B 將是類型 number。

通用類型通常用於創建輔助類型,尤其是在使用映射類型時。 TypeScript 提供了許多預構建的幫助程序類型。 

一個這樣的例子是 Partial 類型,它採用類型 T 並返回另一個與 T 具有相同形狀的類型,但它們的所有字段都設置爲可選。 Partial 的實現如下所示:

type Partial<T> = {
  [P in keyof T]?: T[P];
};

這裏的 Partial 類型接受一個類型,遍歷其屬性類型,然後將它們作爲可選類型返回到新類型中。

注意:由於 Partial 已經內置到 TypeScript 中,因此將此代碼編譯到您的 TypeScript 環境中會重新聲明 Partial 並引發錯誤。 這裏引用的 Partial 的實現只是爲了說明。

要了解泛型類型有多麼強大,假設您有一個對象字面量,用於存儲從一家商店到您的業務分銷網絡中所有其他商店的運輸成本。 每個商店將由一個三字符代碼標識,如下所示:

{
  ABC: {
    ABC: null,
    DEF: 12,
    GHI: 13,
  },
  DEF: {
    ABC: 12,
    DEF: null,
    GHI: 17,
  },
  GHI: {
    ABC: 13,
    DEF: 17,
    GHI: null,
  },
}

該對象是表示商店位置的對象的集合。 在每個商店位置中,都有表示運送到其他商店的成本的屬性。 例如,從 ABC 運往 DEF 的成本是 12。從一家商店到它自己的運費爲空,因爲根本沒有運費。

爲確保其他商店的位置具有一致的值,並且商店運送到自身的始終爲空,您可以創建一個通用的幫助器類型:

type IfSameKeyThanParentTOtherwiseOtherType<Keys extends string, T, OtherType> = {
  [K in Keys]: {
    [SameThanK in K]: T;
  } &
    { [OtherThanK in Exclude<Keys, K>]: OtherType };
};

IfSameKeyThanParentTOtherwiseOtherType 類型接收三個通用類型。 第一個,Keys,是你想要確保你的對象擁有的所有鍵。 在這種情況下,它是所有商店代碼的聯合。 

T 是當嵌套對象字段具有與父對象上的鍵相同的鍵時的類型,在這種情況下,它表示運送到自身的商店位置。 最後,OtherType 是 key 不同時的類型,表示一個商店發貨到另一個商店。

你可以像這樣使用它:

...
type Code = 'ABC' | 'DEF' | 'GHI'
const shippingCosts: IfSameKeyThanParentTOtherwiseOtherType<Code, null, number> = {
  ABC: {
    ABC: null,
    DEF: 12,
    GHI: 13,
  },
  DEF: {
    ABC: 12,
    DEF: null,
    GHI: 17,
  },
  GHI: {
    ABC: 13,
    DEF: 17,
    GHI: null,
  },
}

此代碼現在強制執行類型形狀。 如果您將任何鍵設置爲無效值,TypeScript 將報錯:

...
const shippingCosts: IfSameKeyThanParentTOtherwiseOtherType<Code, null, number> = {
  ABC: {
    ABC: 12,
    DEF: 12,
    GHI: 13,
  },
  DEF: {
    ABC: 12,
    DEF: null,
    GHI: 17,
  },
  GHI: {
    ABC: 13,
    DEF: 17,
    GHI: null,
  },
}

由於 ABC 與自身之間的運費不再爲空,TypeScript 將拋出以下錯誤:

OutputType 'number' is not assignable to type 'null'.(2322)

您現在已經嘗試在接口、類和自定義幫助程序類型中使用泛型。 接下來,您將進一步探討本教程中已經多次出現的主題:使用泛型創建映射類型。

使用泛型創建映射類型

在使用 TypeScript 時,有時您需要創建一個與另一種類型具有相同形狀的類型。 這意味着它應該具有相同的屬性,但屬性的類型設置爲不同的東西。 對於這種情況,使用映射類型可以重用初始類型形狀並減少應用程序中的重複代碼。

在 TypeScript 中,這種結構被稱爲映射類型並依賴於泛型。 在本節中,您將看到如何創建映射類型。

想象一下,您想要創建一個類型,給定另一個類型,該類型返回一個新類型,其中所有屬性都設置爲具有布爾值。 您可以使用以下代碼創建此類型:

type BooleanFields<T> = {
  [K in keyof T]: boolean;
}

在這種類型中,您使用語法 [K in keyof T] 來指定新類型將具有的屬性。 keyof T 運算符用於返回具有 T 中所有可用屬性名稱的聯合。然後使用 K in 語法指定新類型的屬性是返回的聯合類型中當前可用的所有屬性 T 鍵。

這將創建一個名爲 K 的新類型,它綁定到當前屬性的名稱。 這可用於使用語法 T[K] 訪問原始類型中此屬性的類型。 在這種情況下,您將屬性的類型設置爲布爾值。

此 BooleanFields 類型的一個使用場景是創建一個選項對象。 假設您有一個數據庫模型,例如用戶。 

從數據庫中獲取此模型的記錄時,您還將允許傳遞一個指定要返回哪些字段的對象。 

該對象將具有與模型相同的屬性,但類型設置爲布爾值。 在一個字段中傳遞 true 意味着您希望它被返回,而 false 則意味着您希望它被省略。

您可以在現有模型類型上使用 BooleanFields 泛型來返回與模型具有相同形狀的新類型,但所有字段都設置爲布爾類型,如以下突出顯示的代碼所示:

type BooleanFields<T> = {
  [K in keyof T]: boolean;
};
type User = {
  email: string;
  name: string;
}
type UserFetchOptions = BooleanFields<User>;

在此示例中,UserFetchOptions 將與這樣創建它相同:

type UserFetchOptions = {
  email: boolean;
  name: boolean;
}

創建映射類型時,您還可以爲字段提供修飾符。 一個這樣的例子是 TypeScript 中可用的現有泛型類型,稱爲 Readonly。 Readonly 類型返回一個新類型,其中傳遞類型的所有屬性都設置爲只讀屬性。 這種類型的實現如下所示:

type Readonly<T> = {
  readonly [K in keyof T]: T[K]
}

注意:由於 Readonly 已經內置到 TypeScript 中,因此將此代碼編譯到您的 TypeScript 環境中會重新聲明 Readonly 並引發錯誤。 這裏引用的 Readonly 的實現只是爲了說明的目的。

請注意修飾符 readonly,它作爲前綴添加到此代碼中的 [K in keyof T] 部分。 

目前,可以在映射類型中使用的兩個可用修飾符是 readonly 修飾符,它必須作爲前綴添加到屬性,以及 ? 修飾符,可以作爲屬性的後綴添加。 這 ? 修飾符將字段標記爲可選。 

兩個修飾符都可以接收一個特殊的前綴來指定是否應該刪除修飾符 (-) 或添加 (+)。 如果僅提供修飾符,則假定爲 +。

現在您可以使用映射類型基於您已經創建的類型形狀創建新類型,您可以繼續討論泛型的最終用例:條件類型。

使用泛型創建條件類型

在本節中,您將嘗試 TypeScript 中泛型的另一個有用功能:創建條件類型。 首先,您將瞭解條件類型的基本結構。 然後,您將通過創建一個條件類型來探索高級用例,該條件類型省略基於點表示法的對象類型的嵌套字段。

條件類型的基本結構

條件類型是根據某些條件具有不同結果類型的泛型類型。 例如,看看下面的泛型類型 IsStringType

type IsStringType<T> = T extends string ? true : false;

在此代碼中,您正在創建一個名爲 IsStringType 的新泛型類型,它接收單個類型參數 T。在您的類型定義中,您使用的語法看起來像使用 JavaScript 中的三元運算符的條件表達式:T extends string ? 真假。 

此條件表達式正在檢查類型 T 是否擴展了類型字符串。 如果是,則結果類型將是完全正確的類型; 否則,它將被設置爲 false 類型。

注意:此條件表達式是在編譯期間求值的。 TypeScript 僅適用於類型,因此請確保始終將類型聲明中的標識符讀取爲類型,而不是值。 在此代碼中,您使用每個布爾值的確切類型,true 和 false。

要嘗試這種條件類型,請將一些類型作爲其類型參數傳遞:

type IsStringType<T> = T extends string ? true : false;
type A = "abc";
type B = {
  name: string;
};
type ResultA = IsStringType<A>;
type ResultB = IsStringType<B>;

在此代碼中,您創建了兩種類型,A 和 B。類型 A 是字符串文字 “abc” 的類型,而類型 B 是具有名爲 name of type string 屬性的對象的類型。 

然後將這兩種類型與 IsStringType 條件類型一起使用,並將結果類型存儲到兩個新類型 ResultA 和 ResultB 中。

如果檢查 ResultA 和 ResultB 的結果類型,您會注意到 ResultA 類型設置爲準確的類型 true,而 ResultB 類型設置爲 false。 這是正確的,因爲 A 確實擴展了字符串類型而 B 沒有擴展字符串類型,因爲它被設置爲具有字符串類型的單個名稱屬性的對象的類型。

條件類型的一個有用特性是它允許您使用特殊關鍵字 infer 在 extends 子句中推斷類型信息。 然後可以在條件的真實分支中使用這種新類型。 此功能的一種可能用法是檢索任何函數類型的返回類型。

編寫以下 GetReturnType 類型來說明這一點:

type GetReturnType<T> = T extends (...args: any[]) => infer U ? U : never;

在此代碼中,您將創建一個新的泛型類型,它是一個名爲 GetReturnType 的條件類型。 此泛型類型接受單個類型參數 T。

在類型聲明本身內部,您正在檢查類型 T 是否擴展了與函數簽名匹配的類型,該函數簽名接受可變數量的參數(包括零),然後您推斷返回 該函數的類型創建一個新類型 U,可在條件的真實分支內使用。 

U 的類型將綁定到傳遞函數的返回值的類型。 如果傳遞的類型 T 不是函數,則代碼將返回 never 類型。

使用您的類型和以下代碼:

type GetReturnType<T> = T extends (...args: any[]) => infer U ? U : never;
function someFunction() {
  return true;
}
type ReturnTypeOfSomeFunction = GetReturnType<typeof someFunction>;

在此代碼中,您將創建一個名爲 someFunction 的函數,該函數返回 true。 然後使用 typeof 運算符將此函數的類型傳遞給 GetReturnType 泛型,並將結果類型存儲在 ReturnTypeOfSomeFunction 類型中。

由於 someFunction 變量的類型是函數,因此條件類型將評估條件的真實分支。 這將返回類型 U 作爲結果。 

類型 U 是從函數的返回類型推斷出來的,在本例中是布爾值。 如果檢查 ReturnTypeOfSomeFunction 的類型,您會發現它已正確設置爲布爾類型。

高級條件類型用例

條件類型是 TypeScript 中可用的最靈活的功能之一,允許創建一些高級實用程序類型。 

在本節中,您將通過創建一個名爲 NestedOmit<T, KeysToOmit> 的條件類型來探索這些用例之一。 

此實用程序類型將能夠省略對象中的字段,就像現有的 Omit<T, KeysToOmit> 實用程序類型一樣,但也允許使用點表示法省略嵌套字段。

使用新的 NestedOmit<T, KeysToOmit> 泛型,您將能夠使用以下示例中所示的類型:

type SomeType = {
  a: {
    b: string,
    c: {
      d: number;
      e: string[]
    },
    f: number
  }
  g: number | string,
  h: {
    i: string,
    j: number,
  },
  k: {
    l: number,<F3>
  }
}
type Result = NestedOmit<SomeType, "a.b" | "a.c.e" | "h.i" | "k">;

此代碼聲明瞭一個名爲 SomeType 的類型,它具有嵌套屬性的多級結構。 使用 NestedOmit 泛型,傳入類型,然後列出要省略的屬性的鍵。 

請注意如何在第二個類型參數中使用點符號來標識要省略的鍵。 然後將結果類型存儲在 Result 中。

構造此條件類型將使用 TypeScript 中可用的許多功能,例如,模板文字類型、泛型、條件類型和映射類型。

要嘗試這個泛型,首先創建一個名爲 NestedOmit 的泛型類型,它接受兩個類型參數:

type NestedOmit<T extends Record<string, any>, KeysToOmit extends string>

第一個類型參數稱爲 T,它必須是可分配給 Record<string, any> 類型的類型。 這將是您要從中省略屬性的對象的類型。 

第二個類型參數叫做 KeysToOmit,必須是字符串類型。 您將使用它來指定要從類型 T 中省略的鍵。

接下來,通過添加以下突出顯示的代碼來檢查 KeysToOmit 是否可分配給 ${infer KeyPart1}.${infer KeyPart2} 類型:

type NestedOmit<T extends Record<string, any>, KeysToOmit extends string>=
  KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`

在這裏,您使用模板文字字符串類型,同時,利用條件類型推斷模板文字本身內部的其他兩種類型。 

通過推斷模板文字字符串類型的兩個部分,您將字符串拆分爲另外兩個字符串。 第一部分將分配給 KeyPart1 類型,並將包含第一個點之前的所有內容。 

第二部分將分配給 KeyPart2 類型,並將包含第一個點之後的所有內容。 如果您將 “a.b.c” 作爲 KeysToOmit 傳遞,則最初 KeyPart1 將設置爲確切的字符串類型“a”,而 KeyPart2 將設置爲“b.c”。

接下來,您將添加三元運算符來定義條件的第一個真分支:

type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =
  KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`
    ?
      KeyPart1 extends keyof T

這使用 KeyPart1 extends keyof T 來檢查 KeyPart1 是否是給定類型 T 的有效屬性。如果您確實有一個有效的鍵,請添加以下代碼以使條件計算爲兩種類型之間的交集:

type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =
  KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`
    ?
      KeyPart1 extends keyof T
      ?
        Omit<T, KeyPart1>
        & {
          [NewKeys in KeyPart1]: NestedOmit<T[NewKeys], KeyPart2>
        }

Omit<T, KeyPart1> 是一種使用 TypeScript 默認附帶的 Omit 助手構建的類型。 此時,KeyPart1 不是點表示法:它將包含一個字段的確切名稱,該字段包含您希望從原始類型中省略的嵌套字段。 因此,您可以安全地使用現有的實用程序類型。

您正在使用 Omit 刪除 T[KeyPart1] 中的一些嵌套字段,爲此,您必須重建 T[KeyPart1] 的類型。 

爲避免重建整個 T 類型,您使用 Omit 僅從 T 中刪除 KeyPart1,同時保留其他字段。 然後,您將在下一部分的類型中重建 T[KeyPart1]。

[KeyPart1 中的新鍵]:NestedOmit<T[NewKeys], KeyPart2> 是一個映射類型,其中屬性是可分配給 KeyPart1 的屬性,這意味着您剛剛從 KeysToOmit 中提取的部分。 

這是您要刪除的字段的父項。 如果您通過了 a.b.c,在第一次評估您的條件時,它將是 “a” 中的 NewKeys。 

然後將此屬性的類型設置爲遞歸調用 NestedOmit 實用程序類型的結果,但現在使用 T[NewKeys] 將此屬性的類型作爲第一個類型參數傳遞給 T,並作爲第二個類型參數傳遞其餘鍵以點表示法表示,在 KeyPart2 中可用。

在內部條件的 false 分支中,返回綁定到 T 的當前類型,就好像 KeyPart1 不是 T 的有效鍵一樣:

type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =
  KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`
    ?
      KeyPart1 extends keyof T
      ?
        Omit<T, KeyPart1>
        & {
          [NewKeys in KeyPart1]: NestedOmit<T[NewKeys], KeyPart2>
        }
      : T

條件的這個分支意味着你試圖省略一個 T 中不存在的字段。在這種情況下,沒有必要再進一步了。

最後,在外部條件的 false 分支中,使用現有的 Omit 實用程序類型從 Type 中省略 KeysToOmit:

type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =
  KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`
    ?
      KeyPart1 extends keyof T
      ?
        Omit<T, KeyPart1>
        & {
          [NewKeys in KeyPart1]: NestedOmit<T[NewKeys], KeyPart2>
        }
      : T
    : Omit<T, KeysToOmit>;

如果條件 KeysToOmit extends ${infer KeyPart1}.${infer KeyPart2} 爲假,這意味着 KeysToOmit 沒有使用點符號,因此,您可以使用現有的 Omit 實用程序類型。

現在,要使用新的 NestedOmit 條件類型,請創建一個名爲 NestedObject 的新類型:

type NestedObject = {
  a: {
    b: {
      c: number;
      d: number;
    };
    e: number;
  };
  f: number;
};

然後對其調用 NestedOmit 以省略 a.b.c 處可用的嵌套字段:

type Result = NestedOmit<NestedObject, "a.b.c">;

在第一次評估條件類型時,外部條件將爲真,因爲字符串文字類型 “a.b.c” 可分配給模板文字類型“${infer KeyPart1}.${infer KeyPart2}”。 

在這種情況下,KeyPart1 將被推斷爲字符串文字類型 “a”,而 KeyPart2 將被推斷爲字符串的剩餘部分,在本例中爲 “b.c”。

現在將評估內部條件。 這將評估爲真,因爲此時 KeyPart1 是 T 的鍵。KeyPart1 現在是 “a”,而 T 確實有一個屬性 “a”:

type NestedObject = {
a: {
b: {
c: number;
d: number;
};
e: number;
};
  f: number;
};

繼續評估條件,您現在位於內部 true 分支內。 這將構建一個新類型,它是其他兩種類型的交集。 

第一種類型是在 T 上使用 Omit 實用程序類型以省略可分配給 KeyPart1 的字段的結果,在本例中爲 a 字段。 第二種類型是您通過遞歸調用 NestedOmit 構建的新類型。

如果您進行 NestedOmit 的下一次評估,對於第一次遞歸調用,交集類型現在正在構建一個類型以用作 a 字段的類型。 這將重新創建一個沒有您需要省略的嵌套字段的字段。

在 NestedOmit 的最終評估中,第一個條件將返回 false,因爲傳遞的字符串類型現在只是 “c”。 發生這種情況時,您可以使用內置助手從對象中省略該字段。 

這將返回 b 字段的類型,即省略了 c 的原始類型。 現在評估結束,TypeScript 返回您要使用的新類型,並省略嵌套字段。

結論

在本教程中,我們探索適用於函數、接口、類和自定義類型的泛型,以及使用了泛型來創建映射類型和條件類型。 

這些都使泛型成爲您在使用 TypeScript 時可以隨意使用的強大工具。 正確使用它們將使您免於一遍又一遍地重複代碼,並使您編寫的類型更加靈活。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/44fQp4yATvYsd6Ra8k30gw