I am not young enough to know everything.
I am not young enough to know everything.
本文深入聊聊 TypeScript 的一些高级特性,以实现集合转换为交集为主线。
开始之前我们先来看看 lib
中 Object.assign
的类型是如何定义的
assign<T, U>(target: T, source: U): T & U;
assign<T, U, V>(target: T, source1: U, source2: V): T & U & V;
assign<T, U, V, W>(target: T, source1: U, source2: V, source3: W): T & U & V & W;
assign(target: object, ...sources: any[]): any;
意不意外,惊不惊喜。硬编码重载了三种情况,那么超过四个对象之后我们只能得到 any
。
这是由于 TypeScript 的局限性导致的,当然现在 TypeScript 也没有正式解决这个问题,但我们其实已经可以通过 2.8 以后引入的一些特性来 hack 掉这个问题。
type MapTopParameter<U> = U extends any ? (arg: U) => void : never
type IntersectionFromUnion<U> =
MapTopParameter<U> extends (arg: infer T) => void ? T : never
type A = { a: 1 }
type B = { b: 2 }
type C = { c: 2 }
// $ExpectType A & B & C
type Result = IntersectionFromUnion<A | B | C>
要理解这个 hack 需要明白 TypeScript 2.8 引入的两个特性:条件类型(Conditional Types)以及条件类型推导(Type inference in conditional types)。
条件类型可以让我们对类型进行三元运算,根据不同情况返回不同类型
T extends U ? X : Y
但与普通编程语言的三元运算不一样,TypeScript 中还有这么一个特性,叫分布式条件类型(Distributive Conditional Types)。
当 T
是一个集合(Union)的时候,三元运算是对集合中每个元素进行运算,而不是对 T
这个整体进行运算。可以类比为数组中的 map
,对集合进行映射,这相当于往类型系统中加入了遍历功能,并且结合 never
也得到了 filter
的功能。
所以现在 TypeScript 类型系统中有了变量(泛型)、条件控制、循环控制,越来越像一门编程语言了……
利用这个特性,我们看回
type MapTopParameter<U> = U extends any ? (arg: U) => void : never
这里是将集合 U
映射为另外一个以 U
元素为参数的函数集合。
// $ExpectType ((arg: number) => void) | ((arg: 'blog.crimx.com') => void)
type Result = MapTopParameter<number | 'blog.crimx.com'>
这么做有什么用呢,我们接着看。
条件类型推导其实是一种简单的模式匹配,可以类比为正则表达式。
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
这里可以理解为,我们给出了 (...args: any[]) => infer R
这个模板,然后让 T
套这个模板,收集 R
的部分,如这里是收集函数返回的类型。
再结合前面的分布式条件类型,如果 T
是一个集合,那么最后收集的则是各项返回值的集合。
// $ExpectType number | 'blog.crimx.com'
type Result = ReturnType<((arg: string) => number) | (() => 'blog.crimx.com')>
有趣的地方来了,如果我们推导的是函数的参数呢?
type Parameter<T> = T extends (arg: infer P) => any ? P : any
其实也是一样,最后我们会得到一个参数的集合。
type A = { a: number }
type B = { b: 'blog.crimx.com' }
// $ExpectType A | B
type Result = Parameter<((arg: A) => void) | ((arg: B) => void)>
但是!如果我们能想办法阻止这个分布式条件类型,让 T
集合作为一个整体去判断, 这时候表达的是 T
集合中的每一个元素都可以作为 (arg: infer P) => any
的参数使用,也就是说 P
应该是 T
中每个元素的父类,故 P 最后会得到 T
所有元素的交集(Intersection)。
怎么才能达到这个效果呢?
让一个类型成为分布式条件类型其实有一个前提,这个类型必须是无封装的类型参数(naked type parameter),即这个类型推导完成后不能是依然包在其它类型中。
所以我们简单修改一下
type Parameter<T> = [T] extends [(arg: infer P) => any] ? P : any
type A = { a: number }
type B = { b: 'saladict.app' }
// $ExpectType A & B
type Result = Parameter<((arg: A) => void) | ((arg: B) => void)>
成功得到交集了!
当然对于前面的实现我们无需这么做,因为 MapTopParameter
已经是一层封装。
type MapTopParameter<U> = U extends any ? (arg: U) => void : never
type IntersectionFromUnion<U> =
MapTopParameter<U> extends (arg: infer T) => void ? T : never
或者写在一起(略丑)
export type IntersectionFromUnion<TUnion> = (TUnion extends any
? (arg: TUnion) => void
: never) extends (arg: infer TArg) => void
? TArg
: never
这是一个很多人不知道的小特性,将一个元组(tuple)转换为集合。
type tuple = [boolean, 'blog.crimx.com', number]
// $ExpectType number | boolean | "blog.crimx.com"
type union = tuple[number]
最后结合 TypeScript 3.0 加入的 rest 参数,我们定义一个现代版 Object.assign
function objectAssign<TTarget extends object, TSources extends any[]>(
target: TTarget,
...sources: TSources
): IntersectionFromUnion<TTarget | TSources[number]> {
return Object.assign(target, ...sources)
}
const a = objectAssign({ a: 1 }, { b: 2 }, { c: 3 })
// $ExpectType { a: number } & { b: number } & { c: number }
type A = typeof a
通过本文例子的讲解希望能帮助大家深入了解 TypeScript 的一些高级特性,如果有什么感想或问题欢迎留言。
谢谢阅读!
评论没有加载,检查你的局域网
Cannot load comments. Check you network.