推导 Ref 类型

类型体操使我更加强壮。

使用 ref 的姿势为:

const count = ref(0)
console.log(count.value)

所以先摆个 Ref

interface Ref<T = any> {
  value: T
}

推断 infer

inferopen in new window 可在 extends 条件语句中进行推断:

type Flatten<Type> = Type extends Array<infer Item> ? Item : Type

使用泛型来推导结果

ref 函数:

export function ref(value: any) {
  return {
    value
  }
}

const count = ref(1)
type C = typeof count.value // any

加上泛型进行推导:

export function ref<T>(value: T): Ref<T> {
  return {
    value
  }
}

const count = ref(1)
type C = typeof count.value // number
const str = ref('1')
type S = typeof str.value // string

ref 值嵌套

export function ref<T>(value: T): Ref<T> {
  return {
    value
  }
}

const count = ref(ref(ref(1)))
type C = typeof count.value // Ref<Ref<number>>

所以可以作出一个判断,当传入的类型是 Ref 类型时就用其内部的类型,于是得到:

type UnwrapRef<T> = T extends Ref<infer R> ? R : T

配合使用:

type UnwrapRef<T> = T extends Ref<infer R> ? R : T

export function ref<T>(value: T): Ref<UnwrapRef<T>> {
  return {
    value
  } as any
}

// 解说:
// ref(1) 传入类型是 number,所以条件语句 `T extends Ref<infer R>` 会走 false,返回 number
// 于是 ref(1) 的类型为 Ref<number>
// 再 ref(ref(1)),传入类型是 Ref<number>,所以条件语句 `T extends Ref<infer R>` 会走 True,返回 number
// 于是 ref(ref(1)) 的类型依旧为 Ref<number>
// 再 ref(ref(ref(1))),传入类型是 Ref<number>,所以条件语句 `T extends Ref<infer R>` 会走 True,返回 number
const count = ref(ref(ref(1)))
type C = typeof count.value // number
const str = ref(ref(ref('1')))
type S = typeof str.value // string

ref 对象嵌套

修改下示例:

const obj = ref({
  foo: 1,
  bar: '1'
})
type OF = typeof obj.value.foo // number
type OB = typeof obj.value.bar // string

若其中的值是 ref 呢?

const obj = ref({
  foo: ref(1),
  bar: ref('1')
})
type OF = typeof obj.value.foo // Ref<number>
type OB = typeof obj.value.bar // Ref<string>

这就不符合期望了,需要修改 UnwrapRef

type UnwrapRef<T> = T extends Ref<infer R>
  ? UnwrapRefSimple<R>
  : UnwrapRefSimple<T>
                                  
// UnwrapRef 仅作 infer,其余交给 UnwrapRefSimple
type UnwrapRefSimple<T> = T extends Ref
  ? T
  : T extends object
    ? {
      [K in keyof T]: UnwrapRef<T[K]>
    }
    : T

修改测试用例:

const obj = ref({
  foo: ref('1'),
  bar: ref({
    baz: ref(true)
  }),
  arr: [{
    foo: ref(false)
  }, {
    foo: ref('1')
  }]
})

const c1 = ref(1)
type C1 = typeof c1.value // number
const count = ref(ref(ref(1)))
type C = typeof count.value // number
type OF = typeof obj.value.foo // string
type OB = typeof obj.value.bar.baz // boolean

type A = typeof obj.value.arr
obj.value.arr.forEach(a => {
  type A = typeof a.foo // string | boolean
})

对比结果

interface Ref<T = any> {
  value: T
}

type UnwrapRef<T> = T extends Ref<infer R>
  ? UnwrapRefSimple<R>
  : UnwrapRefSimple<T>

type UnwrapRefSimple<T> = T extends Ref
  ? T
  : T extends object
    ? {
      [K in keyof T]: UnwrapRef<T[K]>
    }
    : T
    
export function ref<T>(value: T): Ref<UnwrapRef<T>> {
  return {
    value
  } as any
}

而 Vue 源码则是:

export interface RefUnwrapBailTypes {}

export type UnwrapRef<T> = T extends Ref<infer V>
  ? UnwrapRefSimple<V>
  : UnwrapRefSimple<T>

export type UnwrapRefSimple<T> = T extends
  | Function
  | CollectionTypes
  | BaseTypes
  | Ref
  | RefUnwrapBailTypes[keyof RefUnwrapBailTypes]
  ? T
  : T extends Array<any>
    ? { [K in keyof T]: UnwrapRefSimple<T[K]> }
    : T extends object
      ? {
        [P in keyof T]: P extends symbol ? T[P] : UnwrapRef<T[P]>
      }
      : T

源码中比较有意思的是添加了 RefUnwrapBailTypes,增加了外部扩展的空间。