溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何理解TypeScript中的子類型、逆變、協(xié)變

發(fā)布時間:2021-10-29 15:24:43 來源:億速云 閱讀:193 作者:iii 欄目:web開發(fā)

這篇文章主要介紹“如何理解TypeScript中的子類型、逆變、協(xié)變”,在日常操作中,相信很多人在如何理解TypeScript中的子類型、逆變、協(xié)變問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”如何理解TypeScript中的子類型、逆變、協(xié)變”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!

前言

TypeScript 中有很多地方涉及到子類型 subtype、父類型 supertype  的概念,如果搞不清這些概念,那么很可能被報錯搞得無從下手,或者在寫一些復(fù)雜類型的時候看到別人可以這么寫,但是不知道為什么他可以生效。

子類型

比如考慮如下接口:

interface Animal {   age: number }  interface Dog extends Animal {   bark(): void }

在這個例子中,Animal 是 Dog 的父類,Dog是Animal的子類型,子類型的屬性比父類型更多,更具體。

  • 在類型系統(tǒng)中,屬性更多的類型是子類型。

  • 在集合論中,屬性更少的集合是子集。

也就是說,子類型是父類型的超集,而父類型是子類型的子集,這是直覺上容易搞混的一點。

記住一個特征,子類型比父類型更加具體,這點很關(guān)鍵。

可賦值性 assignable

assignable 是類型系統(tǒng)中很重要的一個概念,當(dāng)你把一個變量賦值給另一個變量時,就要檢查這兩個變量的類型之間是否可以相互賦值。

let animal: Animal let dog: Dog  animal = dog // ?ok dog = animal // ?error! animal 實例上缺少屬性 'bark'

從這個例子里可以看出,animal 是一個「更寬泛」的類型,它的屬性比較少,所以更「具體」的子類型是可以賦值給它的,因為你是知道 animal 上只有  age 這個屬性的,你只會去使用這個屬性,dog 上擁有 animal 所擁有的一切類型,賦值給 animal 是不會出現(xiàn)類型安全問題的。

反之,如果 dog = animal,那么后續(xù)使用者會期望 dog 上擁有 bark 屬性,當(dāng)他調(diào)用了 dog.bark()  就會引發(fā)運行時的崩潰。

從可賦值性角度來說,子類型是可以賦值給父類型的,也就是 父類型變量 = 子類型變量 是安全的,因為子類型上涵蓋了父類型所擁有的的一切屬性。

當(dāng)我初學(xué)的時候,我會覺得 T extends {} 這樣的語句很奇怪,為什么可以 extends  一個空類型并且在傳遞任意類型時都成立呢?當(dāng)搞明白上面的知識點,這個問題也自然迎刃而解了。

在函數(shù)中的運用

假設(shè)我們有這樣的一個函數(shù):

function f(val: { a: number; b: number })

有這樣兩個變量:

let val1 = { a: 1 } let val2 = { a: 1, b: 2, c: 3 }

調(diào)用 f(val1) 是會報錯的,比較顯而易見的來看是因為缺少屬性 b,而函數(shù) f 中很可能去訪問 b 屬性并且做一些操作,比如  b.substr(),這就會導(dǎo)致崩潰。

換成上面的知識點來看,val1 對應(yīng)的類型是{ a: number },它是 { a: number, b: number } 的父類型,調(diào)用  f(val1) 其實就相當(dāng)于把函數(shù)定義中的形參 val 賦值成了 val1, 把父類型的變量賦值給子類型的變量,這是危險的。

反之,調(diào)用 f(val2) 沒有任何問題,因為 val2 的類型是 val類型的子類型,它擁有更多的屬性,函數(shù)有可能使用的一切屬性它都有。

假設(shè)我現(xiàn)在要開發(fā)一個 redux,在聲明 dispatch 類型的時候,我就可以這樣去做:

interface Action {   type: string }  declare function dispatch<T extends Action>(action: T)

這樣,就約束了傳入的參數(shù)一定是 Action 的子類型。也就是說,必須有 type,其他的屬性有沒有,您隨意。

在聯(lián)合類型中的運用

學(xué)習(xí)了以上知識點,再看聯(lián)合類型的可賦值性,乍一看會比較反直覺, 'a' | 'b' | 'c' 是 'a' | 'b'  的子類型嗎?它看起來屬性更多誒?其實正相反,'a' | 'b' | 'c' 是 'a' | 'b'  的父類型。因為前者比后者更「寬泛」,后者比前者更「具體」。

type Parent = 'a' | 'b' | 'c' type Son = 'a' | 'b'  let parent: Parent let son: Son  parent = son // ?ok son = parent // ?error! parent 有可能是 'c'

這里 son 是可以安全的賦值給 parent 的,因為 son 的所有可能性都被 parent 涵蓋了。

而反之則不行,parent 太寬泛了,它有可能是 'c',這是 Son 類型 hold 不住的。

這個例子看完以后,你應(yīng)該可以理解為什么 'a' | 'b' extends 'a' | 'b' | 'c' 為 true 了,在書寫  conditional types的時候更加靈活的運用吧。

逆變和協(xié)變

先來段維基百科的定義[1]:

協(xié)變與逆變(Covariance and contravariance  )是在計算機科學(xué)中,描述具有父/子型別關(guān)系的多個型別通過型別構(gòu)造器、構(gòu)造出的多個復(fù)雜型別之間是否有父/子型別關(guān)系的用語。

描述的比較晦澀難懂,但是用我們上面的動物類型的例子來解釋一波,現(xiàn)在我們還是有 Animal 和 Dog 兩個父子類型。

協(xié)變(Covariance)

那么想象一下,現(xiàn)在我們分別有這兩個子類型的數(shù)組,他們之間的父子關(guān)系應(yīng)該是怎么樣的呢?沒錯,Animal[] 依然是 Dog[]  的父類型,對于這樣的一段代碼,把子類型賦值給父類型依然是安全的:

let animals: Animal[] let dogs: Dog[]  animals = dogs  animals[0].age // ?ok

轉(zhuǎn)變成數(shù)組之后,對于父類型的變量,我們依然只會去 Dog 類型中一定有的那些屬性。

那么,對于 type MakeArray= T[] 這個類型構(gòu)造器來說,它就是 協(xié)變(Covariance) 的。

逆變(Contravariance)

有這樣兩個函數(shù):

let visitAnimal = (animal: Animal) => void; let visitDog = (dog: Dog) => void;

animal = dog 是類型安全的,那么 visitAnimal = visitDog 好像也是可行的?其實不然,想象一下這兩個函數(shù)的實現(xiàn):

let visitAnimal = (animal: Animal) => {   animal.age }  let visitDog = (dog: Dog) => {   dog.age   dog.bark() }

由于 visitDog 的參數(shù)期望的是一個更具體的帶有 bark 屬性的子類型,所以如果 visitAnimal = visitDog  后,我們可能會用一個不帶 bark 屬性的普通的 animal 類型來傳給 visitDog。

visitAnimal = visitDog  let animal = { age: 5 }  visitAnimal(animal) // ?

這會造成運行時錯誤,animal.bark 根本不存在,去調(diào)用這個方法會引發(fā)崩潰。

但是反過來,visitDog = visitAnimal 卻是完全可行的。因為后續(xù)調(diào)用方會傳入一個比 animal 屬性更具體的  dog,函數(shù)體內(nèi)部的一切訪問都是安全的。

在對 Animal 和 Dog 類型分別調(diào)用如下的類型構(gòu)造器之后:

type MakeFunction<T> = (arg: T) => void

父子類型關(guān)系逆轉(zhuǎn)了,這就是 逆變(Contravariance)。

在 TS 中

當(dāng)然,在 TypeScript 中,由于靈活性等權(quán)衡,對于函數(shù)參數(shù)默認(rèn)的處理是 雙向協(xié)變 的。也就是既可以 visitAnimal =  visitDog,也可以 visitDog = visitAnimal。在開啟了 tsconfig 中的 strictFunctionType 后才會嚴(yán)格按照  逆變 來約束賦值關(guān)系。

到此,關(guān)于“如何理解TypeScript中的子類型、逆變、協(xié)變”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI