type NumArr = Array<number>
type U = Unpack<NumArr>
type Unpack<Array<number>> = Array<number> extends Array<infer R> ? R : T
number
仔细看看,是不是有那么点感觉了,它就是对于 extends
后面未知的某些类型进行一个占位 infer R
,后续就可以使用推断出来的 R
这个类型。
巧用 TypeScript(五)-- infer
好了,有了这么多的前置知识,我们来摩拳擦掌尝试实现一下这个 Ref
类型。
我们已经了解到,ref
这个函数就是把一个值包裹成 {value: T}
这样的结构:
我们的目的是,让 ref(ref(ref(2)))
这种嵌套用法,也能顺利的提示出 number 类型。
type Ref<T = any> = {
value: T
function ref<T>(value: T): Ref<T>
const count = ref(2)
count.value // number
默认情况很简单,结合了我们上面提到的几个小知识点很快就能做出来。
如果传入给函数的 value 也是一个 Ref
类型呢?是不是很快就想到 extends
关键字了。
function ref<T>(value: T): T extends Ref
: Ref<UnwrapRef<T>>
先解读 T extends Ref
的情况,如果 value
是 Ref
类型,函数的返回值就原封不动的是这个 Ref
类型。
那么对于 ref(ref(2))
这种类型来说,内层的 ref(2)
返回的是 Ref<number>
类型,
外层的 ref
读取到 ref(Ref<number>)
这个类型以后,
由于此时的 value
符合 extends Ref
的定义,
所以 Ref<number>
又被原封不动的返回了,这就形成了解包。
那么关键点就在于后半段逻辑,Ref<UnwrapRef<T>>
是怎么实现的,
它用来决定 ref(2)
返回的是 Ref<number>
,
并且嵌套的对象 ref({ a: 1 })
,返回 Ref<{ a: number }>
并且嵌套的对象中包含 Ref
类型也会被解包:
const count = ref({
foo: ref('1'),
bar: ref(2)
const count: Ref<{
foo: string;
bar: number;
那么其实本文的关键也就在于,应该如何实现这个 UnwrapRef
解包函数了。
根据我们刚刚学到的 infer
知识,从 Ref
的类型中提取出它的泛型类型并不难:
UnwrapRef
type UnwrapRef<T> = T extends Ref<infer R> ? R : T
UnwrapRef<Ref<number>>
但这只是单层解包,如果 infer R
中的 R
还是 Ref
类型呢?
我们自然的想到了递归声明这个 UnwrapRef
类型:
type UnwrapRef<T> = T extends Ref<infer R>
? UnwrapRef<R>
报错了,不允许循环引用自己!
递归 UnwrapRef
但是到此为止了吗?当然没有,有一种机制可以绕过这个递归限制,那就是配合 索引签名,并且增加其他的能够终止递归的条件,在本例中就是 other
这个索引,它原样返回 T
类型。
type UnwrapRef<T> = {
ref: T extends Ref<infer R> ? R : T
other: T
}[T extends Ref ? 'ref' : 'other']
支持字符串和数字
拆解开来看这个类型,首先假设我们调用了 ref(ref(2))
我们其实会传给 UnwrapRef
一个泛型:
UnwrapRef<Ref<Ref<number>>>
那么第一次走入 [T extends Ref ? 'ref' : 'other']
这个索引的时候,匹配到的是 ref
这个字符串,然后它去
type UnwrapRef<Ref<Ref<number>>> = {
ref: Ref<Ref<number>> extends Ref<infer R> ? UnwrapRef<R> : T
}['ref']
匹配到了 ref
这个索引,然后通过用 Ref<Ref<number>>
去匹配 Ref<infer R>
拿到 R
也就是解包了一层过后的 Ref<number>
。
再次传给 UnwrapRef<Ref<number>>
,又经过同样的逻辑解包后,这次只剩下 number
类型传递了。
也就是 UnwrapRef<number>
,那么这次就不太一样了,索引签名计算出来是 ['other']
,
type UnwrapRef<number> = {
other: number
}['other']
自然就解包得到了 number
这个类型,终止了递归。
考虑一下这种场景:
const count = ref({
foo: ref(1),
bar: ref(2)
那么,count.value.foo
推断的类型应该是 number
,这需要我们用刚刚的遍历索引和 keyof
的知识来做,并且在索引签名中再增加对 object
类型的支持:
type UnwarpRef<T> = {
ref: T extends Ref<infer R> ? R : T
object: { [K in keyof T]: UnwarpRef<T[K]> }
other: T
}[T extends Ref
? 'ref'
: T extends object
? 'object'
: 'other']
这里在遍历 K in keyof T
的时候,只要对值类型 T[K]
再进行解包 UnwarpRef<T[K]>
即可,如果 T[K]
是个 Ref
类型,则会拿到 Ref
的 value
的原始类型。
简化版完整代码
type Ref<T = any> = {
value: T
type UnwarpRef<T> = {
ref: T extends Ref<infer R> ? R : T
object: { [K in keyof T]: UnwarpRef<T[K]> }
other: T
}[T extends Ref
? 'ref'
: T extends object
? 'object'
: 'other']
function ref<T>(value: T): T extends Ref ? T : Ref<UnwarpRef<T>>
在线调戏最终版
这里还是放一下 Vue3 里的源码,在源码中对于数组、对象和计算属性的 ref
也做了相应的处理,但是相信经过了上面简化版的实现后,你对于这个复杂版的原理也可以进一步的掌握了吧。
export interface Ref<T = any> {
[isRefSymbol]: true
value: T
export function ref<T>(value: T): T extends Ref ? T : Ref<UnwrapRef<T>>
export type UnwrapRef<T> = {
cRef: T extends ComputedRef<infer V> ? UnwrapRef<V> : T
ref: T extends Ref<infer V> ? UnwrapRef<V> : T
array: T
object: { [K in keyof T]: UnwrapRef<T[K]> }
}[T extends ComputedRef<any>
? 'cRef'
: T extends Array<any>
? 'array'
: T extends Ref | Function | CollectionTypes | BaseTypes
? 'ref'
: T extends object ? 'object' : 'ref']
乍一看很劝退,没错,我一开始也被这段代码所激励,开始了为期几个月的 TypeScript 恶补生涯。资料真的很难找,这里面涉及的一些高级技巧需要经过反复的练习和实践,才能学下来并且自如的运用出来。
本篇文章之后,相信你对 TypeScript 中的 infer 等高级用法 也有了更深一步的了解,要不要试着挑战一下 力扣的面试题 ?
跟着尤小右学源码只是一个噱头,这个递归类型其实是一位外国人提的一个 pr 去实现的,一开始 TypeScript 不支持递归的时候,尤大写了 9 层手动解包,非常的吓人,可以去这个 pr 里看看,茫茫的一片红。
当然,这也可以看出 TypeScript 是在不断的进步和优化中的,非常期待未来它能够越来越强大。
相信看完本文的你,一定会对上文中提到的一些高级特性有了进一步的掌握。在 Vue3 到来之前,提前学点 TypeScript ,未雨绸缪总是没错的!
关于 TypeScript 的学习路径,我也总结在了我之前的文章 写给初中级前端的高级进阶指南-TypeScript 中给出了很好的资料,大家一起加油吧!
优秀的小册作者修言大佬为前端想学算法的小伙伴们推出了一本零基础也能入门的算法小册,帮助你掌握一些基础算法核心思想或简单算法问题,这本小册我参与了内测过程,也给修言大大提出了很多意见。他的目标就是做面向算法零基础前端人群的「保姆式服务」,非常贴心了~
如果本文对你有帮助,就点个赞支持下吧,你的「赞」是我持续进行创作的动力,让我知道你喜欢看我的文章吧~
❤️感谢大家
关注公众号「前端从进阶到入院」即可加我好友,我拉你进「前端进阶交流群」,大家一起共同交流和进步。
ssh_晨曦时梦见兮
Vue.js
TypeScript
- 9220
-
CUGGZ
JavaScript
Vue.js
- 7.5w
-
前端劝退师
Vue.js
React.js
- 8.6w
-
萌萌哒草头将军
JavaScript
Vue.js
- 2.4w
-
前端劝退师
Vue.js
TypeScript
- 12.7w
-
ssh_晨曦时梦见兮
前端从进阶到入院 @ 字节跳动