[TS 杂谈](1)Promise.all 优雅的类型声明

3,946 阅读4分钟

前言

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跳转定义,定义如下图。

如图所示

可以看到源码类型是通过使用泛型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不会。

myPromiseAll需要通过as const进行参数声明传入元组,Promise.all不需要。

myPromiseAll返回的Promise<readonly [元组元素]Promise.all返回的Promsie<[元组元素]

总代码:

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

结语

人是菜鸡,共同进步,如有错误,多多指教。