天天看點

[TS 雜談](一)Promise.all 優雅的類型聲明

前言

TS 内置的Promise.all,在lib.es2015.promise.d.ts檔案中聲明,通過函數重載定義多個泛型進行類型聲明的。

而在最新的 TS(4.1.3) 中已經有比較優雅的方法進行聲明了,是以這篇文章的作用就是介紹怎麼寫出比較優雅一個Promise.all類型。(不包括函數實作)

前置知識

as const 聲明元組

在某個版本以前,聲明元組隻能通過[string, typeof X, number]一個個手動聲明,而現在可以通過as const進行聲明元組,用法如下:

const tuple = ['你好', '元組', 17] as const//    ^^^^^ = readonly ["你好", "元組", 17]複制代碼      

可以看到這樣就聲明了一個元組,之前的話就得一個個寫元組元素聲明。

映射元組

假設依舊有上面的tuple變量,現在有個需求需要把tuple變量的每個元素都轉成Promise<元素>類型,而這時候就需要使用映射元組的技巧了,文法和映射類型一緻。

type TuplePromise<T> = {
  [K in keyof T]: Promise<T[K]>
}type T1 = TuplePromise<typeof tuple>//   ^^ = readonly [Promise<"你好">, Promise<"元組">, Promise<17>]複制代碼      

類型實作

假設之後都有如下六個類型

const ajax1: Promise<string> = Promise.resolve(':)')
const ajax2: Promise<number> = Promise.resolve(17)
const ajax3: Promise<boolean> = Promise.resolve(true)
const ajax4: string = ':)'
const ajax5: number = 17
const ajax6: boolean = true

const ajaxArr = [ajax1, ajax2, ajax3, ajax4, ajax5, ajax6] as const複制代碼      

原生 Promise.all 類型

在lib.es2015.promise.d.ts檔案中可以找到對應的函數聲明,建議通過 VSC 編輯器中使用ctrl+滑鼠左鍵Promise.all跳轉定義,定義如下圖。

[TS 雜談](一)Promise.all 優雅的類型聲明

可以看到源碼類型是通過使用泛型T進行類型聲明的,源碼中最多參數隻能有10個,因為定義的重載隻有10個,最後一個就是T1-T10,是以當參數超過十個的時候就會報錯。(雖然不會有這個場景)

Promise.all行為,因為使用的泛型,是以可以不用傳入元組,傳入數組也能識别。

Promise.all([ajax1, ajax2, ajax3, ajax4, ajax5, ajax6])
// 這是運作時類型 Promise.all([Promise<string>, Promise<number>, Promise<boolean>, string, number, boolean])
// 傳回 Promise<[string, number, boolean, string, number, boolean]>
  .then(res => {})
  // res: [string, number, boolean, string, number, boolean]複制代碼      

可以看到是Promise的話就會拆出裡面.then參數的類型,如果不是則原樣傳回。通過源碼,我們可以看出是用PromiseLike的類型來進行拆解的,這是因為Promise.all可以使用含有.then的對象。

是以隻要含有.then方法,就要拆出方法參數的類型。

myPromiseAll 類型實作

myPromiseAll類型隻能接受一個元組參數,然後通過元組映射進行拆解,最後傳回Promise<元組映射結果>。

由上一節可以得出我們需要一個類型來提取.then的方法參數類型,這個很簡單,可以使用内置的PromiseLike類型判斷是否含有.then方法且還會自動擷取方法參數類型,是以通過infer可以輕松取出來。

type GetPromiseLikeThenParam<T> = T extends PromiseLike<infer U> ? U : T
type GPLTP<T> = GetPromiseLikeThenParam<T>
// 測試
type T1 = GPLTP<typeof ajax1>
//   ^^ = string
type T2 = GPLTP<typeof ajax4>
//   ^^ = string複制代碼      

映射元組類型進行提取元組每一個PromieLike類型。

type ExtractTuplePromiseLike<T extends ReadonlyArray<unknown>> = {
  [K in keyof T]: GPLTP<T[K]>
}
type ETPL<T extends ReadonlyArray<unknown>> = ExtractTuplePromiseLike<T>

type T1 = ETPL<typeof ajaxArr>複制代碼      

ReadonlyArray<unknown> 相當于 readonly unknown[]。

基礎類型準備就緒,接下來就是寫函數聲明。

函數參數是一個元組,是以聲明參數為ReadonlyArray<unknown>,由于傳回的類型與函數參數有關,是以函數參數要聲明為泛型T,然後傳回就是通過上面的ETPL提取T,然後再用Promise包裝就成功寫好myPromiseAll函數類型

declare function myPromiseAll<T extends ReadonlyArray<unknown>>(
  tuple: T,
): Promise<ETPL<T>>
// 測試
myPromiseAll(ajaxArr)
  .then((res) => {})
  //     ^^^ = readonly [string, number, boolean, string, number, true]複制代碼      

和Promise.all的差別是多了個readonly和變量使用時需要用到as const,如果為了友善可以這麼寫:

myPromiseAll([ajax1, ajax2, ajax3, ajax4, ajax5, ajax6] as const).then((res) => {})複制代碼      

也是完全沒有問題。

總結

myPromiseAll相較于Promise.all的類型還是有些差別的,Promise.all在數組長度超過10的時候會報錯而myPromiseAll不會。

總代碼:

type GetPromiseLikeThenParam<T> = T extends PromiseLike<infer U> ? U : T
type GPLTP<T> = GetPromiseLikeThenParam<T>

type ExtractTuplePromiseLike<T extends ReadonlyArray<unknown>> = {
  [K in keyof T]: GPLTP<T[K]>
}
type ETPL<T extends ReadonlyArray<unknown>> = ExtractTuplePromiseLike<T>

declare function myPromiseAll<T extends ReadonlyArray<unknown>>(
  tuple: T,
): Promise<ETPL<T>>複制代碼      

結語