在 ts 中推断到 tuple 的实践方案
Tuple 保留了数组内容的更多信息,包括数组长度,以及每个元素的类型(尤其是字面量类型)和元素之间的相对位置。
尝试一下 实现 Promise.all 的类型。
一个比较优秀的实现是:
declare function PromiseAll<A extends any[]>(values: readonly [...A]): Promise<{
[key in keyof A]: Awaited<A[key]>
}>
其中 [...A]
用于显式将参数类型推断为 tuple,而不是更宽的数组类型 type[]
。
不过,typescript 标准库里是这样实现的:
/**
* Creates a Promise that is resolved with an array of results when all of the provided Promises
* resolve, or rejected when any Promise is rejected.
* @param values An array of Promises.
* @returns A new Promise.
*/
all<T extends readonly unknown[] | []>(values: T): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>;
注意到它没有使用 [...T]
,而是将泛型 T
的约束变成了一个 union type ... | []
,追加了一个空 tuple 类型。
而当类型中包含 tuple 时,编译器会先一步**推断(inference)该类型为 tuple,尽管在下面这个例子中,参数的类型在可分配性(assignability)**上存在问题,但参数仍然被推断为 tuple。
declare function PromiseAll<T extends (readonly unknown[]) | []>(values: T): Promise<{
-readonly [key in keyof T]: Awaited<T[key]>
}>
const promiseAllTest3 = PromiseAll([1, 2, 3]) // Promise<[number, number, number]>
如果我们去掉这个导致 tuple 推断的 ... | []
,我们会观察到,参数失去了 tuple 的性质,变为了更宽的 number[]
类型。
实际上追加的 tuple 类型是什么并无所谓,即便使用 ... | [string]
,在上面的 case 中也是 work 的。
declare function PromiseAll<T extends readonly unknown[]>(values: T): Promise<{
-readonly [key in keyof T]: Awaited<T[key]>
}>
const promiseAllTest3 = PromiseAll([1, 2, 3]) // Promise<number[]>
所以,作为参数的接收方,我们现在拥有了两个方案来让 ts 编译器将字面量推断到 tuple。
- 使用 spread operator。
- 使用任意 tuple 字面量作为泛型约束,必要时可以使用 union type 来绕过可分配性检查。