前言
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>>
结语
人是菜鸡,共同进步,如有错误,多多指教。