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 中給出了很好的資料,你們一塊兒加油吧!

廣告時間

優秀的小冊做者修言大佬爲前端想學算法的小夥伴們推出了一本零基礎也能入門的算法小冊,幫助你掌握一些基礎算法核心思想或簡單算法問題,這本小冊我參與了內測過程,也給修言大大提出了不少意見。他的目標就是作面向算法零基礎前端人羣的「保姆式服務」,很是貼心了~

求點贊

若是本文對你有幫助,就點個贊支持下吧,你的「贊」是我持續進行創做的動力,讓我知道你喜歡看個人文章吧~

❤️感謝你們

關注公衆號「前端從進階到入院」便可加我好友,我拉你進「前端進階交流羣」,你們一塊兒共同交流和進步。

相關文章
相關標籤/搜索