Vue3 跟着尤雨溪学 TypeScript 之 Ref 类型从零实现

前言

Vue3 中,ref 是一个新出现的 api,不太了解这个 api 的小伙伴能够先看 官方api文档html

简单介绍来讲,响应式的属性依赖一个复杂类型的载体,想象一下这样的场景,你有一个数字 count 须要响应式的改变。前端

const count = reactive(2)

// ❌ 什么鬼
count = 3
复制代码

这样确定是没法触发响应式的,由于 Proxy 须要对一个复杂类型上的某个属性的访问进行拦截,而不是直接拦截一个变量的改变。vue

因而就有了 ref 这个函数,它会为简单类型的值生成一个形为 { value: T } 的包装,这样在修改的时候就能够经过 count.value = 3 去触发响应式的更新了。react

const count = ref(2)

// ✅ (*^▽^*) 彻底能够
count.value = 3
复制代码

那么,ref 函数所返回的类型 Ref,就是本文要讲解的重点了。git

为何说 Ref 是个比较复杂的类型呢?假如 ref 函数中又接受了一个 Ref 类型的参数呢?Vue3 内部实际上是会帮咱们层层解包,只剩下最里层的那个 Ref 类型。github

它是支持嵌套后解包的,最后只会剩下 { value: number } 这个类型。面试

const count = ref(ref(ref(ref(2))))
复制代码

这是一个好几层的嵌套,按理来讲应该是 count.value.value.value.value 才会是 number,可是在 vscode 中,鼠标指向 count.value 这个变量后,提示出的类型就是 number,这是怎么作到的呢?算法

本文尝试给出一种捷径,经过逐步实现这个复杂需求,来倒推出 TS 的高级技巧须要学习哪些知识点。typescript

  1. 泛型的反向推导。
  2. 索引签名
  3. 条件类型
  4. keyof
  5. infer

先逐个拆解这些知识点吧,注意,若是本文中的这些知识点还有所不熟,必定要在代码编辑器中反复敲击调试,刻意练习,也能够在 typescript-playground 中尽情玩耍。api

泛型的反向推导

泛型的正向用法不少人都知道了。

type Value<T> = T

type NumberValue = Value<number>
复制代码

这样,NumberValue 解析出的类型就是 number,其实就相似于类型系统里的传参。

那么反向推导呢?

function create<T>(val: T): T let num: number const c= create(num) 复制代码

在线调试

这里泛型没有传入,竟然也能推断出 value 的类型是 number。

由于 create<T> 这里的泛型 T 被分配给了传入的参数 value: T,而后又用这个 T 直接做为返回的类型,

简单来讲,这里的三个 T 被关联起来了,而且在传入 create(2) 的那一刻,这个 T 被统一推断成了 number。

function create<2>(value: 2): 2 复制代码

阅读资料

具体能够看文档里的泛型章节

索引签名

假设咱们有一个这样的类型:

type Test = {
  foo: number;
  bar: string
}

type N = Test['foo'] // number
复制代码

能够经过相似 JavaScript 中的对象属性查找的语法来找出对应的类型。

具体能够看这里的介绍,有比较详细的例子。

条件类型

假设咱们有一个这样的类型:

type IsNumber<T> = T extends number ? 'yes' : 'no';

type A = IsNumber<2> // yes
type B = isNumber<'3'> // no
复制代码

在线调试

这就是一个典型的条件类型,用 extends 关键字配合三元运算符来判断传入的泛型是否可分配给 extends 后面的类型。

同时也支持多层的三元运算符(后面会用到):

type TypeName<T> = T extends string
  ? "string"
  : T extends boolean
      ? "boolean"
      : "object";

type T0 = TypeName<string>; // "string"
type T1 = TypeName<"a">; // "string"
type T2 = TypeName<true>; // "boolean"
复制代码

阅读资料

具体讲解能够看文档中的 conditional types 部分。

keyof

keyof 操做符是 TS 中用来获取对象的 key 值集合的,好比:

type Obj = {
  foo: number;
  bar: string;
}

type Keys = keyof Obj // "foo" | "bar"
复制代码

这样就轻松获取到了对象 key 值的联合类型:"foo" | "bar"

它也能够用在遍历中:

type Obj = {
  foo: number;
  bar: string;
}

type Copy = {
  [K in keyof Obj]: Obj[K]
}

// Copy 获得和 Obj 如出一辙的类型
复制代码

在线调试

能够看出,遍历的过程当中右侧也能够经过索引直接访问到原类型 Obj 中对应 key 的类型。

阅读资料

index-types

infer

这是一个比较难的点,文档中对它的描述是 条件类型中的类型推断

它的出现使得 ReturnTypeParameters 等一众工具类型的支持都成为可能,是 TypeScript 进阶必须掌握的一个知识点了。

注意前置条件,它必定是出如今条件类型中的。

type Get<T> = T extends infer R ? R: never
复制代码

注意,infer R 的位置表明了一个未知的类型,能够理解为在条件类型中给了它一个占位符,而后就能够在后面的三元运算符中使用它。

type T = Get<number>

// 通过计算
type Get<number> = number extends infer number ? number: never

// 获得
number
复制代码

它的使用很是灵活,它也能够出如今泛型位置:

type Unpack<T> = T extends Array<infer R> ? R : T
复制代码
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 类型。

ref

// 这里用到了泛型的默认值语法 <T = any>
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 ? T : Ref<UnwrapRef<T>> 复制代码

先解读 T extends Ref 的状况,若是 valueRef 类型,函数的返回值就原封不动的是这个 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>> // number
复制代码

但这只是单层解包,若是 infer R 中的 R 仍是 Ref 类型呢?

咱们天然的想到了递归声明这个 UnwrapRef 类型:

// ❌ Type alias 'UnwrapRef' circularly references itself.ts(2456)
type UnwrapRef<T> = T extends Ref<infer R> 
    ? UnwrapRef<R> 
    : T
复制代码

报错了,不容许循环引用本身!

递归 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>>> = {
  // 注意这里和 infer R 对应位置的匹配 获得的是 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 类型,则会拿到 Refvalue 的原始类型。

简化版完整代码

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' // bail out on types that shouldn't be unwrapped
      : T extends object ? 'object' : 'ref']
复制代码

乍一看很劝退,没错,我一开始也被这段代码所激励,开始了为期几个月的 TypeScript 恶补生涯。资料真的很难找,这里面涉及的一些高级技巧须要通过反复的练习和实践,才能学下来而且自如的运用出来。

拓展阅读

本篇文章以后,相信你对 TypeScript 中的 infer 等高级用法 也有了更深一步的了解,要不要试着挑战一下 力扣的面试题

总结

跟着尤小右学源码只是一个噱头,这个递归类型实际上是一位外国人提的一个 pr 去实现的,一开始 TypeScript 不支持递归的时候,尤大写了 9 层手动解包,很是的吓人,能够去这个 pr 里看看,茫茫的一片红。

固然,这也能够看出 TypeScript 是在不断的进步和优化中的,很是期待将来它可以愈来愈强大。

相信看完本文的你,必定会对上文中提到的一些高级特性有了进一步的掌握。在 Vue3 到来以前,提早学点 TypeScript ,未雨绸缪老是没错的!

关于 TypeScript 的学习路径,我也总结在了我以前的文章 写给初中级前端的高级进阶指南-TypeScript 中给出了很好的资料,你们一块儿加油吧!

广告时间

优秀的小册做者修言大佬为前端想学算法的小伙伴们推出了一本零基础也能入门的算法小册,帮助你掌握一些基础算法核心思想或简单算法问题,这本小册我参与了内测过程,也给修言大大提出了不少意见。他的目标就是作面向算法零基础前端人群的「保姆式服务」,很是贴心了~

求点赞

若是本文对你有帮助,就点个赞支持下吧,你的「赞」是我持续进行创做的动力,让我知道你喜欢看个人文章吧~

❤️感谢你们

关注公众号「前端从进阶到入院」便可加我好友,我拉你进「前端进阶交流群」,你们一块儿共同交流和进步。

相关文章
相关标签/搜索