1. 程式人生 > 實用技巧 >白話typescript中的【extends】和【infer】(含vue3的UnwrapRef)

白話typescript中的【extends】和【infer】(含vue3的UnwrapRef)


大家好,我是小雨小雨,致力於分享有趣的、實用的技術文章。




內容分為翻譯和原創,如果有問題,歡迎隨時評論或私信,希望和大家一起進步。




分享不易,希望能夠得到大家的支援和關注。



extends


typescript 2.8引入了條件型別關鍵字: extends,長這個樣子:


T extends U ? X : Y

看起來是不是有點像三元運算子: condition ? result(1) : result(2),用大白話可以表示為:



如果T包含的型別 是 U包含的型別的 '子集',那麼取結果X,否則取結果Y



再舉幾個ts預定義條件型別的例子,加深理解:


type NonNullable<T> = T extends null | undefined ? never : T;

// 如果泛型引數 T 為 null 或 undefined,那麼取 never,否則直接返回T。
let demo1: NonNullable<number>; // => number
let demo2: NonNullable<string>; // => string
let demo3: NonNullable<undefined | null>; // => never

分配式extends


T extends U ? X : Y

其實就是當上面的T為聯合型別的時候,會進行拆分,有點類似數學中的分解因式:



(a + b) * c ⇒ ac + bc



再舉個官網的例子:


type Diff<T, U> = T extends U ? never : T; // 找出T的差集
type Filter<T, U> = T extends U ? T : never; // 找出交集

type T30 = Diff<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'>; // => 'b' | 'd'
// <'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'>
// 相當於
// <'a', 'a' | 'c' | 'f'> |
// <'b', 'a' | 'c' | 'f'> |
// <'c', 'a' | 'c' | 'f'> |
// <'d', 'a' | 'c' | 'f'>
type T31 = Filter<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'>; // => 'a' | 'c'
// <'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'> 同上

let demo1: Diff<number, string>; // => number

我們再來看看infer。


infer


在extends語句中,還支援infer關鍵字,可以推斷一個型別變數,高效的對型別進行模式匹配。但是,這個型別變數只能在true的分支中使用。


// 內建 ReturnType
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

不知道初學ts的朋友們看完這個介紹是不是一臉懵逼,反正之前我是...


其實理解之後很簡單,這裡直接說下我的理解,應該還算簡單易懂:



infer X 就相當於聲明瞭一個變數,這個變數隨後可以使用,是不是有點像for迴圈裡面的宣告語句?



for (let i = 0, len = arr.length; i < len; i++) {
// do something
}


不同的是,infer X的這個位置本應該有一個寫死的型別變數,只不過用infer R替換了,更靈活。




需要注意的是infer宣告的這個變數只能在true分支中使用



還是舉幾個例子,加深理解,紙上談兵終覺淺嘛:


例子一


// 解讀: 如果泛型變數T是 () => infer R的`子集`,那麼返回 通過infer獲取到的函式返回值,否則返回boolean型別
type Func<T> = T extends () => infer R ? R : boolean;

let func1: Func<number>; // => boolean
let func2: Func<''>; // => boolean
let func3: Func<() => Promise<number>>; // => Promise<number>

例子二


// 同上,但當a、b為不同型別的時候,返回不同型別的聯合型別
type Obj<T> = T extends {a: infer VType, b: infer VType} ? VType : number;

let obj1: Obj<string>; // => number
let obj2: Obj<true>; // => number
let obj3: Obj<{a: number, b: number}>; // => number
let obj4: Obj<{a: number, b: () => void}>; // => number | () => void

例子三(Vue3中的UnwrapRef)


// 如果泛型變數T是ComputedRef的'子集',那麼使用UnwrapRefSimple處理infer指代的ComputedRef泛型引數V
// 否則進一步判斷是否為Ref的'子集',進一步UnwrapRefSimple
export type UnwrapRef<T> = T extends ComputedRef<infer V>
? UnwrapRefSimple<V>
: T extends Ref<infer V> ? UnwrapRefSimple<V> : UnwrapRefSimple<T>

// 我是分割線

// 如果T為Function | CollectionTypes | BaseTypes | Ref之一的'子集',直接返回。
// 否則判斷是否為陣列的'子集',不是的話視為object,呼叫UnwrappedObject
type UnwrapRefSimple<T> = T extends Function | CollectionTypes | BaseTypes | Ref
? T
: T extends Array<any> ? T : T extends object ? UnwrappedObject<T> : T

// 我是分割線
// 呼叫UnwrapRef,產生遞迴效果,解決了ts型別遞迴
type UnwrappedObject<T> = { [P in keyof T]: UnwrapRef<T[P]> } & SymbolExtract<T>

// 我是分割線

// 泛型Ref
export interface Ref<T = any> {
[Symbol()]: true
value: T
}

// 我是分割線

export interface ComputedRef<T = any> extends WritableComputedRef<T> {
readonly value: T
}

// 我是分割線

export interface WritableComputedRef<T> extends Ref<T> {
readonly effect: ReactiveEffect<T>
}

建議自己捋一遍。


總結


ts提供的extends和infer大大增加了型別判斷的靈活性和複用性,雖然用與不用都可以,但能熟練地使用高階特性將大大提升ts推斷的效率和程式碼型別的可讀性。


如有問題,歡迎指出。


勞動節快樂!