题主这里的[1, 'hello', { a: 1 }]
是一个数组字面量 ,先说一下当开发者写下一些字面量的时候,TypeScript是怎么推导出对应类型的。
字面量的类型推断一直是不好设计的,因为同一个字面量可以被推断成不同的类型,test(1)里的第一个参数类型可以被推断成 1
、1 | 2
、number
甚至是其他。
所以字面量类型推断需要配合 上下文类型 来协组推断,举例来说,你写了
declare function test1<T>(a:T,b:T):any; test1(1,2)
那么上面的a和b都会被推断成number,这是TypeScript认为最符合开发者预期的类型,但是当你改成
declare function test2<T extends number>(a:T,b:T):any; test2(1,2)
此时a 和 b都会被推断成1 | 2
这样更窄的类型,这里就是 上下文类型 协组字面量来做类型推断了。
回到题主的问题,题主的例子
function watch<T extends unknown[]>(source: [...T], cb: (params: T) => void){}; const watcher = watch([1, 'hello', { a: 1 }], (params)=>{})
题主可能认为,这里的 source 的[...T]
只是说把source
类型推断成一个Tuple Type
,而 T
本身应该是一个形如(number | string | {a: number})[]
的 Array Type
。
但是这里的字面量[1, 'hello', { a: 1 }]
并不是独立推断的, 它受到上下文类型
的影响,而这个影响是是 Variadic tuple types 设计的一项特性。
当一个数组字面量的上下文类型是 Tuple Type,那么就会对这个数组字面量推导出对应的Tuple Type,[...T]
就是这个上下文类型的指示器。
TS的作者 Anders 老爷子在提交这项特性的PR里提到过这一特点,并给出示例:
declare function ft1<T extends unknown[]>(t: T): T; declare function ft2<T extends unknown[]>(t: T): readonly [...T]; declare function ft3<T extends unknown[]>(t: [...T]): T; declare function ft4<T extends unknown[]>(t: [...T]): readonly [...T]; ft1(['hello', 42]); // (string | number)[] ft2(['hello', 42]); // readonly (string | number)[] ft3(['hello', 42]); // [string, number] ft4(['hello', 42]); // readonly [string, number]
可以注意到这里的 [...T]
作为Tuple Type
的指示器,它改编了对 数组字面量 原始类型的推断,有了这个上下文类型,['hello', 42]
的原始类型不再被推断成 (string | number)[]
,而是被推成了 [string, number]
。
考虑这样的一个例子
declare function test3<T extends unknown[]>(t: [string,...T]): T; test3(['hello', 42]); // [number]
这里的T类型是[number]
,这是因为['hello', 42]
在看到上下文类型是Tuple Like
的时候,就把字面量类型推断成了[string,number]
这种Tuple类型。然后再利用[string,number] extends [string,...infer T]
来找出T
的类型,也就是[number]
。
那么数组的字面量,除了被推断成Array Type<Union Type>
和Tuple Type
以外,还会有其它的结果吗?答案暂时是否定的,它目前只有这两种推断方式。
在当下的checker.ts
源代码里可以找到对应的实现function checkArrayLiteral
if (forceTuple || inConstContext || contextualType && someType(contextualType, isTupleLikeType)) { return createArrayLiteralType(createTupleType(elementTypes, elementFlags, /*readonly*/ inConstContext)); } return createArrayLiteralType(createArrayType(elementTypes.length ? getUnionType(sameMap(elementTypes, (t, i) => elementFlags[i] & ElementFlags.Variadic ? getIndexedAccessTypeOrUndefined(t, numberType) || anyType : t), UnionReduction.Subtype) : strictNullChecks ? implicitNeverType : undefinedWideningType, inConstContext));
可以看出来,在特定的条件里(比如上下文类型是isTupleLikeType
),数组的字面量可以被推断成Tuple Type
,否则它会被推断成 Array Type<Union Type>
。
结语:字面量的推断充满着大量的人工设计,因为一个字面量的类型拥有无限的可能性,TypeScript 在推导字面量的时候,对开发者的开发体验非常看重,它想着尽可能的让推断出来的类型符合开发者的预期 ,但是有时候不容易做到,上下文类型相当于给 TypeScript 多一些提示,让它推出来的结果更符合开发者的预期。