如何在 TypeScript 中使用泛型
大廠技術(shù) 高級前端 Node進階
點擊上方 程序員成長指北,關(guān)注公眾號
回復1,加入高級Node交流群
英文 | https://www.digitalocean.com/community/tutorials/how-to-use-generics-in-typescript
一個環(huán)境。我們可以執(zhí)行 TypeScript 程序以跟隨示例。要在本地計算機上進行設(shè)置,我們將需要準備以下內(nèi)容。
為了運行處理 TypeScript 相關(guān)包的開發(fā)環(huán)境,同時安裝了 Node 和 npm(或 yarn)。本文教程中使用 Node.js 版本 為14.3.0 和 npm 版本 6.14.5 進行了測試。要在 macOS 或 Ubuntu 18.04 上安裝,請按照如何在 macOS 上安裝 Node.js 和創(chuàng)建本地開發(fā)環(huán)境或如何在 Ubuntu 18.04 上安裝 Node.js 的使用 PPA 安裝部分中的步驟進行操作。如果您使用的是適用于 Linux 的 Windows 子系統(tǒng) (WSL),這也適用。
此外,我們需要在機器上安裝 TypeScript 編譯器 (tsc)。為此,請參閱官方 TypeScript 網(wǎng)站。
如果你不想在本地機器上創(chuàng)建 TypeScript 環(huán)境,你可以使用官方的 TypeScript Playground 來跟隨。
您將需要足夠的 JavaScript 知識,尤其是 ES6+ 語法,例如解構(gòu)、rest 運算符和導入/導出。如果您需要有關(guān)這些主題的更多信息,建議閱讀我們的如何用 JavaScript 編寫代碼系列。
本文教程將參考支持 TypeScript 并顯示內(nèi)聯(lián)錯誤的文本編輯器的各個方面。這不是使用 TypeScript 所必需的,但確實可以更多地利用 TypeScript 功能。為了獲得這些好處,您可以使用像 Visual Studio Code 這樣的文本編輯器,它完全支持開箱即用的 TypeScript。你也可以在 TypeScript Playground 中嘗試這些好處。
function pickObjectKeys(obj, keys) {let result = {}for (const key of keys) {if (key in obj) {result[key] = obj[key]}}return result}
此代碼段顯示了 pickObjectKeys() 函數(shù),該函數(shù)遍歷keys數(shù)組并使用數(shù)組中指定的鍵創(chuàng)建一個新對象。
下面是一個展示如何使用該函數(shù)的示例:
const language = {name: "TypeScript",age: 8,extensions: ['ts', 'tsx']}const ageAndExtensions = pickObjectKeys(language, ['age', 'extensions'])
這聲明了一種對象,然后使用 pickObjectKeys() 函數(shù)隔離 age 和 extensions 屬性。 ageAndExtensions 的值如下:
{age: 8,extensions: ['ts', 'tsx']}
如果要將此代碼遷移到 TypeScript 以使其類型安全,則必須使用泛型。 我們可以通過添加以下突出顯示的行來重構(gòu)代碼:
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> 為函數(shù)聲明了兩個參數(shù)類型,其中 K 被分配一個類型,該類型是 T 中的key的并集。
然后將 obj 函數(shù)參數(shù)設(shè)置為 T 表示的任何類型,并將key設(shè)置為數(shù)組, 無論 K 代表什么類型。
由于在語言對象的情況下 T 將 age 設(shè)置為數(shù)字并將 extensions 設(shè)置為字符串數(shù)組,因此,變量 ageAndExtensions 現(xiàn)在將被分配具有屬性 age: number 和 extensions: string[] 的對象的類型。
這會根據(jù)提供給 pickObjectKeys 的參數(shù)強制執(zhí)行返回類型,從而允許函數(shù)在知道需要強制執(zhí)行的特定類型之前靈活地強制執(zhí)行類型結(jié)構(gòu)。
當在 Visual Studio Code 等 IDE 中使用該函數(shù)時,這也增加了更好的開發(fā)人員體驗,它將根據(jù)您提供的對象為 keys 參數(shù)創(chuàng)建建議。 這顯示在以下屏幕截圖中:

了解如何在 TypeScript 中創(chuàng)建泛型后,您現(xiàn)在可以繼續(xù)探索在特定情況下使用泛型。 本教程將首先介紹如何在函數(shù)中使用泛型。
將泛型與函數(shù)一起使用
將泛型與函數(shù)一起使用的最常見場景之一是當您有一些代碼不容易為所有用例鍵入時。 為了使該功能適用于更多情況,您可以包括泛型類型。
在此步驟中,您將運行一個恒等函數(shù)示例來說明這一點。 您還將探索一個異步示例,了解何時將類型參數(shù)直接傳遞給您的泛型,以及如何為您的泛型類型參數(shù)創(chuàng)建約束和默認值。
分配通用參數(shù)
看一下下面的函數(shù),它返回作為第一個參數(shù)傳入的內(nèi)容:
function identity(value) {return value;}
function identity<T>(value: T): T{return value;}
你把你的函數(shù)變成了一個泛型函數(shù),它接受泛型類型參數(shù) T,這是第一個參數(shù)的類型,然后將返回類型設(shè)置為與 : T 相同。
接下來,添加以下代碼來試用該功能:
function identity<T>(value: T): T {return value;}const result = identity(123);
結(jié)果的類型為 123,這是您傳入的確切數(shù)字。這里的 TypeScript 從調(diào)用代碼本身推斷泛型類型。 這樣調(diào)用代碼不需要傳遞任何類型參數(shù)。 您也可以顯式地將泛型類型參數(shù)設(shè)置為您想要的類型:
function identity<T>(value: T): T {return value;}const result = identity<number>(123);
在此代碼中,result 具有類型編號。 通過使用 <number> 代碼傳入類型,您明確地讓 TypeScript 知道您希望身份函數(shù)的泛型類型參數(shù) T 的類型為 number。 這將強制將數(shù)字類型作為參數(shù)和返回值。
直接傳遞類型參數(shù)
直接傳遞類型參數(shù)在使用自定義類型時也很有用。 例如,看看下面的代碼:
type ProgrammingLanguage = {name: string;};function identity<T>(value: T): T {return value;}const result = identity<ProgrammingLanguage>({ name: "TypeScript" });
在此代碼中,result 具有自定義類型 ProgrammingLanguage,因為它直接傳遞給標識函數(shù)。 如果您沒有明確包含類型參數(shù),則結(jié)果將具有類型 { name: string } 。
使用 JavaScript 時的另一個常見示例是使用包裝函數(shù)從 API 檢索數(shù)據(jù):
async function fetchApi(path: string) {const response = await fetch(`https://example.com/api${path}`)return response.json();}
此異步函數(shù)將 URL 路徑作為參數(shù),使用 fetch API 向 URL 發(fā)出請求,然后返回 JSON 響應(yīng)值。 在這種情況下,fetchApi 函數(shù)的返回類型將是 Promise<any>,這是對 fetch 的響應(yīng)對象調(diào)用 json() 的返回類型。
將 any 作為返回類型并不是很有幫助。 any 表示任何 JavaScript 值,使用它你將失去靜態(tài)類型檢查,這是 TypeScript 的主要優(yōu)點之一。 如果您知道 API 將返回給定形狀的對象,則可以使用泛型使此函數(shù)類型安全:
async function fetchApi<ResultType>(path: string): Promise<ResultType>{const response = await fetch(`https://example.com/api${path}`);return response.json();}
突出顯示的代碼將您的函數(shù)轉(zhuǎn)換為接受 ResultType 泛型類型參數(shù)的泛型函數(shù)。 此泛型類型用于函數(shù)的返回類型:Promise<ResultType>。
注意:由于您的函數(shù)是異步的,因此,您必須返回一個 Promise 對象。 TypeScript Promise 類型本身是一種通用類型,它接受 promise 解析為的值的類型。
如果仔細查看您的函數(shù),您會發(fā)現(xiàn)參數(shù)列表或 TypeScript 能夠推斷其值的任何其他地方都沒有使用泛型。 這意味著調(diào)用代碼在調(diào)用您的函數(shù)時必須顯式傳遞此泛型的類型。
以下是檢索用戶數(shù)據(jù)的 fetchApi 通用函數(shù)的可能實現(xiàn):
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 {}
在此代碼中,您將創(chuàng)建一個名為 User 的新類型,并使用該類型的數(shù)組 (User[]) 作為 ResultType 泛型參數(shù)的類型。 數(shù)據(jù)變量現(xiàn)在具有類型 User[] 而不是任何。
注意:當您使用 await 異步處理函數(shù)的結(jié)果時,返回類型將是 Promise<T> 中 T 的類型,在本例中是通用類型 ResultType。
默認類型參數(shù)
像您一樣創(chuàng)建通用的 fetchApi 函數(shù),調(diào)用代碼始終必須提供類型參數(shù)。 如果調(diào)用代碼不包含泛型類型,則 ResultType 將綁定為未知。 以下面的實現(xiàn)為例:
async function fetchApi<ResultType>(path: string): Promise<ResultType> {const response = await fetch(`https://example.com/api${path}`);returnresponse.json();}const data = await fetchApi('/users')console.log(data.a)export {}
此代碼嘗試訪問數(shù)據(jù)的理論上的屬性。 但由于數(shù)據(jù)類型未知,這段代碼將無法訪問對象的屬性。
如果您不打算將特定類型添加到泛型函數(shù)的每次調(diào)用中,則可以將默認類型添加到泛型類型參數(shù)中。 這可以通過在泛型類型之后添加 = 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 {}
使用此代碼,您不再需要在調(diào)用 fetchApi 函數(shù)時將類型傳遞給 ResultType 泛型參數(shù),因為它具有默認類型 Record<string, any>。 這意味著 TypeScript 會將數(shù)據(jù)識別為具有字符串類型的鍵和任意類型的值的對象,從而允許您訪問其屬性。
類型參數(shù)約束
在某些情況下,泛型類型參數(shù)需要只允許將某些形狀傳遞給泛型。 要為您的泛型創(chuàng)建額外的特殊層,您可以對您的參數(shù)施加約束。
假設(shè)您有一個存儲限制,您只能存儲所有屬性都具有字符串值的對象。 為此,您可以創(chuàng)建一個函數(shù),它接受任何對象并返回另一個對象,該對象具有與原始對象相同的鍵,但所有值都轉(zhuǎn)換為字符串。 這個函數(shù)將被稱為 stringifyObjectKeyValues。
這個函數(shù)將是一個通用函數(shù)。 這樣,您就可以使生成的對象具有與原始對象相同的形狀。 該函數(shù)將如下所示:
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 數(shù)組方法迭代原始鍵數(shù)組,將值字符串化并將它們添加到新數(shù)組中。
為確保調(diào)用代碼始終將對象傳遞給您的函數(shù),您在泛型類型 T 上使用類型約束,如以下突出顯示的代碼所示:
function stringifyObjectKeyValues<Textends Record<string, any>>(obj: T) {// ...}
extends Record<string, any> 被稱為泛型類型約束,它允許您指定您的泛型類型必須可分配給 extends 關(guān)鍵字之后的類型。
在這種情況下,Record<string, any> 表示一個具有字符串類型的鍵和任意類型的值的對象。 您可以讓您的類型參數(shù)擴展任何有效的 TypeScript 類型。
在調(diào)用 reduce 時,reducer 函數(shù)的返回類型基于累加器的初始值。 {} as { [K in keyof T]: string } 代碼通過對空對象 {} 進行類型轉(zhuǎn)換,將累加器初始值的類型設(shè)置為 { [K in keyof T]: string }。
type { [K in keyof T]: string } 創(chuàng)建一個新類型,它具有與 T 相同的鍵,但所有值都設(shè)置為字符串類型,這稱為映射類型,本教程將在后面的部分中進一步探討。
以下代碼顯示了 stringifyObjectKeyValues 函數(shù)的實現(xiàn):
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;}
這將確保返回值與函數(shù)的目的一致。
本節(jié)介紹了將泛型與函數(shù)一起使用的多種方法,包括直接分配類型參數(shù)以及為參數(shù)形狀設(shè)置默認值和約束。
接下來,您將通過一些示例來了解泛型如何使接口和類適用于更多情況。
將泛型與接口、類和類型一起使用
在 TypeScript 中創(chuàng)建接口和類時,使用泛型類型參數(shù)來設(shè)置結(jié)果對象的形狀會很有用。
例如,一個類可能具有不同類型的屬性,具體取決于傳遞給構(gòu)造函數(shù)的內(nèi)容。 在本節(jié)中,您將了解在類和接口中聲明泛型類型參數(shù)的語法,并檢查 HTTP 應(yīng)用程序中的常見用例。
通用接口和類
要創(chuàng)建通用接口,您可以在接口名稱之后添加類型參數(shù)列表:
interface MyInterface<T> {field: T}
這聲明了一個接口,該接口具有一個屬性字段,其類型由傳遞給 T 的類型確定。
對于類,語法幾乎相同:
class MyClass<T> {field: Tconstructor(field: T) {this.field = field}}
通用接口/類的一個常見用例是當您有一個字段,其類型取決于客戶端代碼如何使用接口/類時。
假設(shè)您有一個 HttpApplication 類,用于處理對 API 的 HTTP 請求,并且某些上下文值將傳遞給每個請求處理程序。 這樣做的一種方法是:
class HttpApplication<Context> {context: Contextconstructor(context: Context) {this.context = context;}// ... implementationget(url: string, handler: (context: Context) => Promise<void>): this {// ... implementationreturn this;}}
此類存儲一個上下文,其類型作為 get 方法中處理函數(shù)的參數(shù)類型傳入。 在使用過程中,傳遞給 get 處理程序的參數(shù)類型將從傳遞給類構(gòu)造函數(shù)的內(nèi)容中正確推斷出來。
...const context = { someValue: true };const app = new HttpApplication(context);app.get('/api', async () => {console.log(context.someValue)});
在此實現(xiàn)中,TypeScript 會將 context.someValue 的類型推斷為布爾值。
通用類型
現(xiàn)在已經(jīng)了解了類和接口中泛型的一些示例,您現(xiàn)在可以繼續(xù)創(chuàng)建泛型自定義類型。 將泛型應(yīng)用于類型的語法類似于將泛型應(yīng)用于接口和類的語法。 看看下面的代碼:
type MyIdentityType<T> = T此泛型類型返回作為類型參數(shù)傳遞的類型。 假設(shè)您使用以下代碼實現(xiàn)了這種類型:
...type B = MyIdentityType<number>
在這種情況下,類型 B 將是類型 number。
通用類型通常用于創(chuàng)建輔助類型,尤其是在使用映射類型時。 TypeScript 提供了許多預構(gòu)建的幫助程序類型。
一個這樣的例子是 Partial 類型,它采用類型 T 并返回另一個與 T 具有相同形狀的類型,但它們的所有字段都設(shè)置為可選。 Partial 的實現(xiàn)如下所示:
type Partial<T> = {[P in keyof T]?: T[P];};
這里的 Partial 類型接受一個類型,遍歷其屬性類型,然后將它們作為可選類型返回到新類型中。
注意:由于 Partial 已經(jīng)內(nèi)置到 TypeScript 中,因此將此代碼編譯到您的 TypeScript 環(huán)境中會重新聲明 Partial 并引發(fā)錯誤。 這里引用的Partial的實現(xiàn)只是為了說明。
要了解泛型類型有多么強大,假設(shè)您有一個對象字面量,用于存儲從一家商店到您的業(yè)務(wù)分銷網(wǎng)絡(luò)中所有其他商店的運輸成本。 每個商店將由一個三字符代碼標識,如下所示:
{ABC: {ABC: null,DEF: 12,GHI: 13,},DEF: {ABC: 12,DEF: null,GHI: 17,},GHI: {ABC: 13,DEF: 17,GHI: null,},}
該對象是表示商店位置的對象的集合。 在每個商店位置中,都有表示運送到其他商店的成本的屬性。 例如,從 ABC 運往 DEF 的成本是 12。從一家商店到它自己的運費為空,因為根本沒有運費。
為確保其他商店的位置具有一致的值,并且商店運送到自身的始終為空,您可以創(chuàng)建一個通用的幫助器類型:
type IfSameKeyThanParentTOtherwiseOtherType<Keys extends string, T, OtherType> = {[K in Keys]: {[SameThanK in K]: T;} &{ [OtherThanK in Exclude<Keys, K>]: OtherType };};
IfSameKeyThanParentTOtherwiseOtherType 類型接收三個通用類型。 第一個,Keys,是你想要確保你的對象擁有的所有鍵。 在這種情況下,它是所有商店代碼的聯(lián)合。
T 是當嵌套對象字段具有與父對象上的鍵相同的鍵時的類型,在這種情況下,它表示運送到自身的商店位置。 最后,OtherType 是 key 不同時的類型,表示一個商店發(fā)貨到另一個商店。
你可以像這樣使用它:
...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,},}
此代碼現(xiàn)在強制執(zhí)行類型形狀。 如果您將任何鍵設(shè)置為無效值,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)您現(xiàn)在已經(jīng)嘗試在接口、類和自定義幫助程序類型中使用泛型。 接下來,您將進一步探討本教程中已經(jīng)多次出現(xiàn)的主題:使用泛型創(chuàng)建映射類型。
使用泛型創(chuàng)建映射類型
在使用 TypeScript 時,有時您需要創(chuàng)建一個與另一種類型具有相同形狀的類型。 這意味著它應(yīng)該具有相同的屬性,但屬性的類型設(shè)置為不同的東西。 對于這種情況,使用映射類型可以重用初始類型形狀并減少應(yīng)用程序中的重復代碼。
在 TypeScript 中,這種結(jié)構(gòu)被稱為映射類型并依賴于泛型。 在本節(jié)中,您將看到如何創(chuàng)建映射類型。
想象一下,您想要創(chuàng)建一個類型,給定另一個類型,該類型返回一個新類型,其中所有屬性都設(shè)置為具有布爾值。 您可以使用以下代碼創(chuàng)建此類型:
type BooleanFields<T> = {[K in keyof T]: boolean;}
在這種類型中,您使用語法 [K in keyof T] 來指定新類型將具有的屬性。 keyof T 運算符用于返回具有 T 中所有可用屬性名稱的聯(lián)合。然后使用 K in 語法指定新類型的屬性是返回的聯(lián)合類型中當前可用的所有屬性 T鍵。
這將創(chuàng)建一個名為 K 的新類型,它綁定到當前屬性的名稱。 這可用于使用語法 T[K] 訪問原始類型中此屬性的類型。 在這種情況下,您將屬性的類型設(shè)置為布爾值。
此 BooleanFields 類型的一個使用場景是創(chuàng)建一個選項對象。 假設(shè)您有一個數(shù)據(jù)庫模型,例如用戶。
從數(shù)據(jù)庫中獲取此模型的記錄時,您還將允許傳遞一個指定要返回哪些字段的對象。
該對象將具有與模型相同的屬性,但類型設(shè)置為布爾值。 在一個字段中傳遞 true 意味著您希望它被返回,而 false 則意味著您希望它被省略。
您可以在現(xiàn)有模型類型上使用 BooleanFields 泛型來返回與模型具有相同形狀的新類型,但所有字段都設(shè)置為布爾類型,如以下突出顯示的代碼所示:
type BooleanFields<T> = {[K in keyof T]: boolean;};type User = {email: string;name: string;}type UserFetchOptions = BooleanFields<User>;
在此示例中,UserFetchOptions 將與這樣創(chuàng)建它相同:
type UserFetchOptions = {email: boolean;name: boolean;}
創(chuàng)建映射類型時,您還可以為字段提供修飾符。 一個這樣的例子是 TypeScript 中可用的現(xiàn)有泛型類型,稱為 Readonly<T>。 Readonly<T> 類型返回一個新類型,其中傳遞類型的所有屬性都設(shè)置為只讀屬性。 這種類型的實現(xiàn)如下所示:
type Readonly<T> = {readonly [K in keyof T]: T[K]}
注意:由于 Readonly 已經(jīng)內(nèi)置到 TypeScript 中,因此將此代碼編譯到您的 TypeScript 環(huán)境中會重新聲明 Readonly 并引發(fā)錯誤。 這里引用的Readonly的實現(xiàn)只是為了說明的目的。
請注意修飾符 readonly,它作為前綴添加到此代碼中的 [K in keyof T] 部分。
目前,可以在映射類型中使用的兩個可用修飾符是 readonly 修飾符,它必須作為前綴添加到屬性,以及 ? 修飾符,可以作為屬性的后綴添加。 這 ? 修飾符將字段標記為可選。
兩個修飾符都可以接收一個特殊的前綴來指定是否應(yīng)該刪除修飾符 (-) 或添加 (+)。 如果僅提供修飾符,則假定為 +。
現(xiàn)在您可以使用映射類型基于您已經(jīng)創(chuàng)建的類型形狀創(chuàng)建新類型,您可以繼續(xù)討論泛型的最終用例:條件類型。
使用泛型創(chuàng)建條件類型
在本節(jié)中,您將嘗試 TypeScript 中泛型的另一個有用功能:創(chuàng)建條件類型。 首先,您將了解條件類型的基本結(jié)構(gòu)。 然后,您將通過創(chuàng)建一個條件類型來探索高級用例,該條件類型省略基于點表示法的對象類型的嵌套字段。
條件類型的基本結(jié)構(gòu)
條件類型是根據(jù)某些條件具有不同結(jié)果類型的泛型類型。 例如,看看下面的泛型類型 IsStringType<T>:
type IsStringType<T> = T extends string ? true : false;在此代碼中,您正在創(chuàng)建一個名為 IsStringType 的新泛型類型,它接收單個類型參數(shù) T。在您的類型定義中,您使用的語法看起來像使用 JavaScript 中的三元運算符的條件表達式:T extends string ? 真假。
此條件表達式正在檢查類型 T 是否擴展了類型字符串。 如果是,則結(jié)果類型將是完全正確的類型; 否則,它將被設(shè)置為 false 類型。
注意:此條件表達式是在編譯期間求值的。 TypeScript 僅適用于類型,因此請確保始終將類型聲明中的標識符讀取為類型,而不是值。 在此代碼中,您使用每個布爾值的確切類型,true 和 false。
要嘗試這種條件類型,請將一些類型作為其類型參數(shù)傳遞:
type IsStringType<T> = T extends string ? true : false;type A = "abc";type B = {name: string;};type ResultA = IsStringType<A>;type ResultB = IsStringType<B>;
在此代碼中,您創(chuàng)建了兩種類型,A 和 B。類型 A 是字符串文字“abc”的類型,而類型 B 是具有名為 name of type string 屬性的對象的類型。
然后將這兩種類型與 IsStringType 條件類型一起使用,并將結(jié)果類型存儲到兩個新類型 ResultA 和 ResultB 中。
如果檢查 ResultA 和 ResultB 的結(jié)果類型,您會注意到 ResultA 類型設(shè)置為準確的類型 true,而 ResultB 類型設(shè)置為 false。 這是正確的,因為 A 確實擴展了字符串類型而 B 沒有擴展字符串類型,因為它被設(shè)置為具有字符串類型的單個名稱屬性的對象的類型。
條件類型的一個有用特性是它允許您使用特殊關(guān)鍵字 infer 在 extends 子句中推斷類型信息。 然后可以在條件的真實分支中使用這種新類型。 此功能的一種可能用法是檢索任何函數(shù)類型的返回類型。
編寫以下 GetReturnType 類型來說明這一點:
type GetReturnType<T> = T extends (...args: any[]) => infer U ? U : never;在此代碼中,您將創(chuàng)建一個新的泛型類型,它是一個名為 GetReturnType 的條件類型。 此泛型類型接受單個類型參數(shù) T。
在類型聲明本身內(nèi)部,您正在檢查類型 T 是否擴展了與函數(shù)簽名匹配的類型,該函數(shù)簽名接受可變數(shù)量的參數(shù)(包括零),然后您推斷返回 該函數(shù)的類型創(chuàng)建一個新類型 U,可在條件的真實分支內(nèi)使用。
U 的類型將綁定到傳遞函數(shù)的返回值的類型。 如果傳遞的類型 T 不是函數(shù),則代碼將返回 never 類型。
使用您的類型和以下代碼:
type GetReturnType<T> = T extends (...args: any[]) => infer U ? U : never;function someFunction() {return true;}type ReturnTypeOfSomeFunction = GetReturnType<typeof someFunction>;
在此代碼中,您將創(chuàng)建一個名為 someFunction 的函數(shù),該函數(shù)返回 true。 然后使用 typeof 運算符將此函數(shù)的類型傳遞給 GetReturnType 泛型,并將結(jié)果類型存儲在 ReturnTypeOfSomeFunction 類型中。
由于 someFunction 變量的類型是函數(shù),因此條件類型將評估條件的真實分支。 這將返回類型 U 作為結(jié)果。
類型 U 是從函數(shù)的返回類型推斷出來的,在本例中是布爾值。 如果檢查 ReturnTypeOfSomeFunction 的類型,您會發(fā)現(xiàn)它已正確設(shè)置為布爾類型。
高級條件類型用例
條件類型是 TypeScript 中可用的最靈活的功能之一,允許創(chuàng)建一些高級實用程序類型。
在本節(jié)中,您將通過創(chuàng)建一個名為 NestedOmit<T, KeysToOmit> 的條件類型來探索這些用例之一。
此實用程序類型將能夠省略對象中的字段,就像現(xiàn)有的 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 的類型,它具有嵌套屬性的多級結(jié)構(gòu)。 使用 NestedOmit 泛型,傳入類型,然后列出要省略的屬性的鍵。
請注意如何在第二個類型參數(shù)中使用點符號來標識要省略的鍵。 然后將結(jié)果類型存儲在 Result 中。
構(gòu)造此條件類型將使用 TypeScript 中可用的許多功能,例如,模板文字類型、泛型、條件類型和映射類型。
要嘗試這個泛型,首先創(chuàng)建一個名為 NestedOmit 的泛型類型,它接受兩個類型參數(shù):
type NestedOmit<T extends Record<string, any>, KeysToOmit extends string>第一個類型參數(shù)稱為 T,它必須是可分配給 Record<string, any> 類型的類型。 這將是您要從中省略屬性的對象的類型。
第二個類型參數(shù)叫做KeysToOmit,必須是字符串類型。 您將使用它來指定要從類型 T 中省略的鍵。
接下來,通過添加以下突出顯示的代碼來檢查 KeysToOmit 是否可分配給 ${infer KeyPart1}.${infer KeyPart2} 類型:
type NestedOmit<T extends Record<string, any>, KeysToOmit extends string>=KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`
在這里,您使用模板文字字符串類型,同時,利用條件類型推斷模板文字本身內(nèi)部的其他兩種類型。
通過推斷模板文字字符串類型的兩個部分,您將字符串拆分為另外兩個字符串。 第一部分將分配給 KeyPart1 類型,并將包含第一個點之前的所有內(nèi)容。
第二部分將分配給 KeyPart2 類型,并將包含第一個點之后的所有內(nèi)容。 如果您將“a.b.c”作為 KeysToOmit 傳遞,則最初 KeyPart1 將設(shè)置為確切的字符串類型“a”,而 KeyPart2 將設(shè)置為“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 的有效屬性。如果您確實有一個有效的鍵,請?zhí)砑右韵麓a以使條件計算為兩種類型之間的交集:
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 助手構(gòu)建的類型。 此時,KeyPart1 不是點表示法:它將包含一個字段的確切名稱,該字段包含您希望從原始類型中省略的嵌套字段。 因此,您可以安全地使用現(xiàn)有的實用程序類型。
您正在使用 Omit 刪除 T[KeyPart1] 中的一些嵌套字段,為此,您必須重建 T[KeyPart1] 的類型。
為避免重建整個 T 類型,您使用 Omit 僅從 T 中刪除 KeyPart1,同時保留其他字段。 然后,您將在下一部分的類型中重建 T[KeyPart1]。
[KeyPart1 中的新鍵]:NestedOmit<T[NewKeys], KeyPart2> 是一個映射類型,其中屬性是可分配給 KeyPart1 的屬性,這意味著您剛剛從 KeysToOmit 中提取的部分。
這是您要刪除的字段的父項。 如果您通過了 a.b.c,在第一次評估您的條件時,它將是“a”中的 NewKeys。
然后將此屬性的類型設(shè)置為遞歸調(diào)用 NestedOmit 實用程序類型的結(jié)果,但現(xiàn)在使用 T[NewKeys] 將此屬性的類型作為第一個類型參數(shù)傳遞給 T,并作為第二個類型參數(shù)傳遞其余鍵以點表示法表示,在 KeyPart2 中可用。
在內(nèi)部條件的 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 分支中,使用現(xiàn)有的 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 沒有使用點符號,因此,您可以使用現(xiàn)有的 Omit 實用程序類型。
現(xiàn)在,要使用新的 NestedOmit 條件類型,請創(chuàng)建一個名為 NestedObject 的新類型:
type NestedObject = {a: {b: {c: number;d: number;};e: number;};f: number;};
然后對其調(diào)用 NestedOmit 以省略 a.b.c 處可用的嵌套字段:
type Result = NestedOmit<NestedObject, "a.b.c">;在第一次評估條件類型時,外部條件將為真,因為字符串文字類型“a.b.c”可分配給模板文字類型“${infer KeyPart1}.${infer KeyPart2}”。
在這種情況下,KeyPart1 將被推斷為字符串文字類型“a”,而 KeyPart2 將被推斷為字符串的剩余部分,在本例中為“b.c”。
現(xiàn)在將評估內(nèi)部條件。 這將評估為真,因為此時 KeyPart1 是 T 的鍵。KeyPart1 現(xiàn)在是“a”,而 T 確實有一個屬性“a”:
type NestedObject = {a: {b: {c: number;d: number;};e: number;};f: number;};
繼續(xù)評估條件,您現(xiàn)在位于內(nèi)部 true 分支內(nèi)。 這將構(gòu)建一個新類型,它是其他兩種類型的交集。
第一種類型是在 T 上使用 Omit 實用程序類型以省略可分配給 KeyPart1 的字段的結(jié)果,在本例中為 a 字段。 第二種類型是您通過遞歸調(diào)用 NestedOmit 構(gòu)建的新類型。
如果您進行 NestedOmit 的下一次評估,對于第一次遞歸調(diào)用,交集類型現(xiàn)在正在構(gòu)建一個類型以用作 a 字段的類型。 這將重新創(chuàng)建一個沒有您需要省略的嵌套字段的字段。
在 NestedOmit 的最終評估中,第一個條件將返回 false,因為傳遞的字符串類型現(xiàn)在只是“c”。 發(fā)生這種情況時,您可以使用內(nèi)置助手從對象中省略該字段。
這將返回 b 字段的類型,即省略了 c 的原始類型。 現(xiàn)在評估結(jié)束,TypeScript 返回您要使用的新類型,并省略嵌套字段。
結(jié)論
在本教程中,我們探索適用于函數(shù)、接口、類和自定義類型的泛型,以及使用了泛型來創(chuàng)建映射類型和條件類型。
這些都使泛型成為您在使用 TypeScript 時可以隨意使用的強大工具。 正確使用它們將使您免于一遍又一遍地重復代碼,并使您編寫的類型更加靈活。
以上就是我今天跟你分享的全部內(nèi)容,希望這些內(nèi)容對你有所幫助。
Node 社群 我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學習感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關(guān)的交流、學習、共建。下方加 考拉 好友回復「Node」即可。
“分享、點贊、在看” 支持一波??
