TypeScript 類型兼容(逆變、協(xié)變、雙向協(xié)變和不變)的實(shí)現(xiàn)
在 TypeScript 中,類型系統(tǒng)支持“逆變(Contravariance)”、“協(xié)變(Covariance)”、“雙向協(xié)變(Bivariance)”和“不變(Invariance)”的概念,這些概念主要用于理解類型之間的兼容性,尤其是在函數(shù)參數(shù)和返回值之間的關(guān)系。
類型安全和型變
首先,TypeScript 通過(guò)在 JavaScript 上添加靜態(tài)類型系統(tǒng)來(lái)實(shí)現(xiàn)類型安全。類型安全意味著我們可以在編譯時(shí)檢測(cè)到可能的類型錯(cuò)誤,防止它們?cè)谶\(yùn)行時(shí)導(dǎo)致程序崩潰。例如,TypeScript 不允許將一個(gè) number 類型的值賦給一個(gè) boolean 類型的變量,也不允許調(diào)用某個(gè)對(duì)象上不存在的方法。
let isDone: boolean = true; isDone = 1; // Error: Type '1' is not assignable to type 'boolean'. let currentDate: Date = new Date(); currentDate.exec(); // Error: Property 'exec' does not exist on type 'Date'.
在這兩個(gè)例子中,TypeScript 的類型檢查機(jī)制會(huì)在編譯時(shí)報(bào)告錯(cuò)誤,從而保證了類型的正確性和代碼的可靠性。
然而,完全嚴(yán)格的類型安全有時(shí)會(huì)導(dǎo)致不便。例如,當(dāng)我們處理子類型和父類型時(shí),TypeScript 允許一些 變通
,以便在保證類型安全的前提下提供更大的靈活性。這種 變通
被稱為型變(Variance)。
在類型系統(tǒng)中,當(dāng)類型 A 和類型 B 存在繼承關(guān)系時(shí),使用 A 的地方是否也能使用 B。型變?cè)诤瘮?shù)的參數(shù)和返回值中表現(xiàn)尤為明顯。型變通常包括以下四種類型:
- 協(xié)變(Covariance):如果 A 是 B 的子類型,那么
F<A>
也是F<B>
的子類型。換句話說(shuō),F<A>
可以賦值給F<B>
。協(xié)變通常用于函數(shù)的返回值類型。 - 逆變(Contravariance):如果 A 是 B 的子類型,那么
F<B>
是F<A>
的子類型。換句話說(shuō),F<B>
可以賦值給F<A>
。逆變通常用于函數(shù)的參數(shù)類型。
雙向協(xié)變(Bivariance):TypeScript 中的一種特殊情況,允許函數(shù)參數(shù)類型既可以協(xié)變也可以逆變,雖然這種情況理論上不安全,但它是為了保持實(shí)際代碼的兼容性。
不變(Invariance):F<A>
既不是 F<B>
的子類型,也不是其超類型。換句話說(shuō),F<A>
和 F<B>
是完全不兼容的類型。
協(xié)變
在 TypeScript 中,協(xié)變 主要是指在處理泛型、函數(shù)類型、數(shù)組等結(jié)構(gòu)時(shí),允許子類型被視為其父類型。簡(jiǎn)言之,協(xié)變?cè)试S子類型可以賦值給父類型,這是最常見(jiàn)的類型兼容性規(guī)則。
泛型默認(rèn)是協(xié)變的,這就以為著如果 B 是 A 的子類型,那么 T<B>
也是 T<A>
的子類型。
例如,假設(shè)我們有一個(gè)泛型接口 Box<T>
:
class Animal {} class Dog extends Animal {} interface Box<T> { value: T; } let animalBox: Box<Animal>; let dogBox: Box<Dog> = { value: new Dog() }; animalBox = dogBox; // 合法,協(xié)變
他們相互賦值是不會(huì)錯(cuò)的,雖然這倆類型不一樣,但是依然是類型安全的。
在這個(gè)例子中,Box<Dog>
可以賦值給 Box<Animal>
,因?yàn)?Dog 是 Animal 的子類型。這種關(guān)系在 TypeScript 的類型系統(tǒng)中是允許的,因?yàn)樗蠀f(xié)變的規(guī)則。
除了在類中,函數(shù)和數(shù)組中也都是可以協(xié)變的:
class Animal {} class Dog extends Animal {} type AnimalFactory = () => Animal; type DogFactory = () => Dog; let createAnimal: AnimalFactory; let createDog: DogFactory = () => new Dog(); createAnimal = createDog; // 合法,協(xié)變 class Animal {} class Dog extends Animal {} let dogs: Dog[] = [new Dog(), new Dog()]; let animals: Animal[]; animals = dogs; // 合法,協(xié)變
協(xié)變?cè)?TypeScript 中非常重要,因?yàn)樗试S類型系統(tǒng)具有更大的靈活性。協(xié)變?cè)试S你在不違反類型安全的情況下,靈活地使用繼承結(jié)構(gòu)中的子類型和父類型。
在使用泛型數(shù)據(jù)結(jié)構(gòu)時(shí),協(xié)變?cè)试S我們?cè)诓挥绊戭愋桶踩那闆r下,處理更廣泛的類型。例如,List<Dog>
可以在 List<Animal>
的上下文中使用,因?yàn)?Dog 是 Animal 的子類型。協(xié)變使得函數(shù)可以返回更具體的類型而不影響函數(shù)的兼容性,這有助于設(shè)計(jì)靈活且類型安全的接口。例如,你可以在接口的實(shí)現(xiàn)中返回一個(gè)更具體的類型,而不需要更改接口本身的簽名。
雖然協(xié)變提供了很多靈活性,但是在某些情況下也可能會(huì)帶來(lái)問(wèn)題,例如數(shù)組的協(xié)變可能會(huì)導(dǎo)致運(yùn)行時(shí)錯(cuò)誤,例如在使用 push 操作的時(shí)候,它可能會(huì)破壞數(shù)組的類型一致性,從而導(dǎo)致運(yùn)行時(shí)報(bào)錯(cuò):
class Animal {} class Dog extends Animal {} class Cat extends Animal {} let animals: Animal[] = [new Dog()]; animals.push(new Cat()); // 合法,但可能不是預(yù)期的行為
在這個(gè)例子中,animals 原本是 Dog[] 類型,但由于數(shù)組的協(xié)變,Cat 實(shí)例被推入了這個(gè)數(shù)組,這可能會(huì)導(dǎo)致一些不一致的問(wèn)題。
逆變
在 TypeScript 中,逆變 是指,如果類型 A 是類型 B 的父類型(A <: B),那么在函數(shù)參數(shù)的位置上,類型 B 可以賦值給類型 A。換句話說(shuō),逆變?cè)试S父類型的函數(shù)參數(shù)賦值給子類型的函數(shù)參數(shù)。
在函數(shù)參數(shù)中的逆變,我們可以考慮以下類繼承結(jié)構(gòu):
class Animal { speak() { console.log("Animal sound"); } } class Dog extends Animal { speak() { console.log("Bark"); } }
假設(shè)我們有兩個(gè)函數(shù)類型,一個(gè)接受 Animal 作為參數(shù),另一個(gè)接受 Dog 作為參數(shù):
type AnimalHandler = (animal: Animal) => void; type DogHandler = (dog: Dog) => void;
在逆變的情況下,AnimalHandler 可以賦值給 DogHandler。這是因?yàn)?Dog 是 Animal 的子類型,因此處理 Animal 的函數(shù)也可以處理 Dog。
let handleAnimal: AnimalHandler = (animal: Animal) => { animal.speak(); }; let handleDog: DogHandler; handleDog = handleAnimal; // 合法,逆變
在這個(gè)例子中,handleAnimal 可以處理任何 Animal,包括 Dog。因此,我們可以將 handleAnimal 賦值給 handleDog,這在類型系統(tǒng)中是安全的。
逆變的直觀理解是,你可以將處理較寬泛類型的函數(shù)賦值給處理更具體類型的函數(shù)。如果一個(gè)函數(shù)能夠處理 Animal,那么它肯定能夠處理 Dog,因?yàn)?Dog 是 Animal 的一種特殊化形式。
在 TypeScript 中,型變通常用于描述類型在不同上下文中的傳遞方式。逆變與協(xié)變是相對(duì)的:
- 協(xié)變:子類型可以賦值給父類型。例如,Dog 是 Animal 的子類型,那么 Dog 類型的值可以賦值給 Animal 類型的變量。
- 逆變:父類型可以賦值給子類型。例如,Animal 是 Dog 的父類型,那么接受 Animal 參數(shù)的函數(shù)可以賦值給接受 Dog 參數(shù)的函數(shù)。
在 React 中,事件處理函數(shù)是逆變的一種典型應(yīng)用。當(dāng)你在一個(gè)通用的事件處理器中使用更廣泛的事件類型時(shí),逆變可以確保你的事件處理器適用于子類型事件。
例如,考慮一個(gè)處理 MouseEvent 的事件處理器:
type MouseEventHandler = (event: React.MouseEvent<HTMLButtonElement>) => void;
如果我們有一個(gè)更通用的事件處理函數(shù),它可以處理任意的 DOM 事件,那么它可以被安全地用作處理特定的 MouseEvent:
type AnyEventHandler = (event: React.SyntheticEvent) => void; const handleEvent: AnyEventHandler = (event) => { console.log(event); }; const handleMouseEvent: MouseEventHandler = handleEvent; // 合法,逆變
在這個(gè)例子中,handleEvent 可以處理任何 SyntheticEvent,其中包括 MouseEvent,所以它可以被賦值給 handleMouseEvent。這就是逆變的一個(gè)實(shí)際應(yīng)用:更廣泛的處理函數(shù)可以用于處理更具體的事件類型。
在使用高階組件時(shí),逆變也可能發(fā)揮作用。高階組件(HOC)是接受一個(gè)組件并返回一個(gè)新組件的函數(shù)。在處理高階組件時(shí),如果傳遞給 HOC 的組件的屬性是某種類型的父類型,那么 HOC 返回的組件可以安全地接受更具體的子類型屬性。
假設(shè)你有一個(gè)高階組件 withLogging,它可以將日志功能添加到任何組件中:
function withLogging<P>(Component: React.ComponentType<P>): React.FC<P> { return (props: P) => { console.log("Rendering", Component.name); return <Component {...props} />; }; }
如果你有一個(gè)組件 DogComponent 接受 Dog 類型的屬性,那么你可以將 withLogging 應(yīng)用于 DogComponent,并返回一個(gè)同樣接受 Dog 屬性的組件:
interface Dog { name: string; breed: string; } const DogComponent: React.FC<Dog> = ({ name, breed }) => ( <div> {name} is a {breed} </div> ); const LoggedDogComponent = withLogging(DogComponent); // 使用 LoggedDogComponent <LoggedDogComponent name="Rex" breed="Labrador" />;
在這里,withLogging 的參數(shù)類型是泛型 P,而 Dog 是 P 的一個(gè)具體實(shí)例。由于 TypeScript 支持逆變,因此即使 P 是父類型,它也可以接受子類型 Dog 的參數(shù),這使得 LoggedDogComponent 可以安全地使用 Dog 類型的屬性。
逆變 是 TypeScript 中的一種型變,允許父類型的函數(shù)參數(shù)賦值給子類型的函數(shù)參數(shù)。它保證了類型安全,因?yàn)樘幚砀割愋偷暮瘮?shù)可以適用于子類型,而不會(huì)引發(fā)類型錯(cuò)誤。這通常用于函數(shù)參數(shù)類型的兼容性處理,確保函數(shù)的靈活性和擴(kuò)展性。
雙向協(xié)變
在類型系統(tǒng)中,協(xié)變?cè)试S子類型賦值給父類型,逆變則允許父類型賦值給子類型。雙向協(xié)變是 TypeScript 中的一種特殊行為,它允許函數(shù)參數(shù)的類型既可以協(xié)變,也可以逆變。
簡(jiǎn)單來(lái)說(shuō),雙向協(xié)變?cè)试S你在處理函數(shù)參數(shù)類型時(shí),既可以將子類型賦值給父類型,也可以將父類型賦值給子類型。
假設(shè)我們有兩個(gè)類 Animal 和 Dog,Dog 繼承自 Animal:
class Animal { speak() { console.log("Animal sound"); } } class Dog extends Animal { speak() { console.log("Bark"); } }
現(xiàn)在我們定義兩個(gè)函數(shù)類型,一個(gè)接受 Dog 作為參數(shù),另一個(gè)接受 Animal 作為參數(shù):
type DogHandler = (dog: Dog) => void; type AnimalHandler = (animal: Animal) => void;
根據(jù)雙向協(xié)變的規(guī)則,TypeScript 允許我們將 DogHandler 賦值給 AnimalHandler,也允許我們將 AnimalHandler 賦值給 DogHandler:
let handleDog: DogHandler = (dog: Dog) => { dog.speak(); }; let handleAnimal: AnimalHandler = (animal: Animal) => { animal.speak(); }; handleAnimal = handleDog; // 合法,協(xié)變 handleDog = handleAnimal; // 合法,逆變
在這個(gè)例子中,handleAnimal 可以處理 Animal 類型的參數(shù),而 handleDog 只處理 Dog 類型的參數(shù)。盡管從理論上來(lái)說(shuō),將 handleAnimal 賦值給 handleDog 可能存在類型安全問(wèn)題(handleAnimal 可能接受 Cat,而 handleDog 只能處理 Dog),但 TypeScript 允許這種賦值操作。
考慮一個(gè)更復(fù)雜的例子,其中我們有一個(gè)函數(shù)接受另一個(gè)函數(shù)作為參數(shù):
function processAnimal(handler: (a: Animal) => void): void { const animal = new Animal(); handler(animal); } function processDog(handler: (d: Dog) => void): void { const dog = new Dog(); handler(dog); }
根據(jù)雙向協(xié)變的規(guī)則,processDog 可以接受 processAnimal 中的函數(shù)作為參數(shù),反之亦然:
processDog((dog: Dog) => { console.log(dog.speak()); }); // 合法 processAnimal((animal: Animal) => { console.log(animal.speak()); }); // 合法
在這里,盡管 processAnimal 接受的是 Animal 類型的參數(shù),而 processDog 只處理 Dog 類型,但 TypeScript 允許這種相互賦值。
雖然雙向協(xié)變使得代碼更靈活,但它也可能引發(fā)類型安全問(wèn)題。特別是在處理繼承關(guān)系較為復(fù)雜的情況下,可能會(huì)導(dǎo)致運(yùn)行時(shí)錯(cuò)誤。
function handleAnyAnimal(animal: Animal): void { console.log("Handling an animal"); } function handleOnlyDog(dog: Dog): void { console.log("Handling a dog"); } let dogHandler: (d: Dog) => void = handleAnyAnimal; // 合法,但可能不安全 dogHandler(new Dog()); // 正常 dogHandler(new Animal()); // 運(yùn)行時(shí)錯(cuò)誤,因?yàn)?Animal 不是 Dog
在這個(gè)例子中,將 handleAnyAnimal 賦值給 dogHandler 是合法的,因?yàn)?TypeScript 允許這種雙向協(xié)變。然而,當(dāng)我們嘗試傳遞一個(gè) Animal(而不是 Dog)給 dogHandler 時(shí),可能會(huì)引發(fā)運(yùn)行時(shí)錯(cuò)誤。
不變
在類型系統(tǒng)中,不變 是指某個(gè)類型不能在子類型和父類型之間相互替換。具體來(lái)說(shuō),如果你有一個(gè)泛型類型 T<A>
和 T<B>
,即使 A 是 B 的子類型,T<A>
也不能賦值給 T<B>
,反之亦然。這種嚴(yán)格的類型匹配規(guī)則被稱為不變。
考慮一個(gè)簡(jiǎn)單的泛型類:
class Animal { name: string; } class Dog extends Animal { breed: string; } class Cat extends Animal { color: string; } interface Box<T> { content: T; } let animalBox: Box<Animal> = { content: new Animal() }; let dogBox: Box<Dog> = { content: new Dog() }; let catBox: Box<Cat> = { content: new Cat() };
在不變的規(guī)則下,盡管 Dog 是 Animal 的子類型,但 Box<Dog>
不能賦值給 Box<Animal>
,同樣 Box<Animal>
也不能賦值給 Box<Dog>
:
animalBox = dogBox; // 錯(cuò)誤,Box<Dog> 不能賦值給 Box<Animal> dogBox = animalBox; // 錯(cuò)誤,Box<Animal> 不能賦值給 Box<Dog>
這就是不變的體現(xiàn)。TypeScript 要求 Box<Animal>
和 Box<Dog>
必須是完全一致的類型,任何嘗試在這些類型之間進(jìn)行賦值都會(huì)導(dǎo)致類型錯(cuò)誤。
不變是一種型變規(guī)則,要求類型在所有上下文中必須嚴(yán)格匹配。比如我想要一只鴨子,你不能給我一個(gè)碗對(duì)吧,碗又不能吃。
總結(jié)
本文詳細(xì)介紹了 TypeScript 類型系統(tǒng)中的四種型變概念:協(xié)變、逆變、雙向協(xié)變 和 不變,以及它們?cè)陬愋桶踩挽`活性方面的作用。
- 協(xié)變:允許子類型賦值給父類型,常見(jiàn)于函數(shù)的返回值和數(shù)組類型。協(xié)變使得類型系統(tǒng)更加靈活,例如,
List<Dog>
可以在需要List<Animal>
的地方使用。 - 逆變:允許父類型賦值給子類型,常見(jiàn)于函數(shù)的參數(shù)類型。逆變確保了類型系統(tǒng)的靈活性和安全性,比如將處理
Animal
的函數(shù)賦值給處理Dog
的函數(shù)。 - 雙向協(xié)變:一種特殊情況,允許函數(shù)參數(shù)類型既可以協(xié)變也可以逆變,盡管這可能引發(fā)類型安全問(wèn)題。雙向協(xié)變?cè)?TypeScript 中存在主要是為了保持與 JavaScript 的兼容性。
- 不變:要求類型在所有上下文中嚴(yán)格匹配,不能在子類型和父類型之間相互替換。這種嚴(yán)格的類型檢查確保了類型系統(tǒng)的安全性,但也減少了靈活性。
理解這些型變概念能夠幫助開(kāi)發(fā)者在 TypeScript 中編寫(xiě)既靈活又安全的代碼,尤其是在處理復(fù)雜的類型關(guān)系和函數(shù)參數(shù)時(shí)。它們?yōu)槲覀兲峁┝艘粋€(gè)平衡類型安全性和代碼靈活性的工具。
到此這篇關(guān)于TypeScript 類型兼容(逆變、協(xié)變、雙向協(xié)變和不變)的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)TypeScript 類型兼容內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javascript HTML5 canvas實(shí)現(xiàn)打磚塊游戲
這篇文章主要介紹了基于javascript HTML5 canvas實(shí)現(xiàn)打磚塊游戲的具體實(shí)現(xiàn)代碼,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-04-04JavaScript關(guān)于prototype實(shí)例詳解(超重點(diǎn))
prototype是js里面給類增加功能擴(kuò)展的一種模式,這篇文章主要介紹了JavaScript關(guān)于prototype(超重點(diǎn)),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-08-08微信小程序使用slider設(shè)置數(shù)據(jù)值及switch開(kāi)關(guān)組件功能【附源碼下載】
這篇文章主要介紹了微信小程序使用slider設(shè)置數(shù)據(jù)值及switch開(kāi)關(guān)組件功能,結(jié)合實(shí)例形式分析了slider組件及switch組件的功能與使用方法,并附帶源碼供讀者下載參考,需要的朋友可以參考下2017-12-12JavaScript日期庫(kù)date-fn.js使用方法解析
這篇文章主要介紹了JavaScript日期庫(kù)date-fn.js使用方法解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09JavaScript關(guān)于提高網(wǎng)站性能的幾點(diǎn)建議(一)
這篇文章主要介紹了JavaScript關(guān)于提高網(wǎng)站性能的幾點(diǎn)建議(一)的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-07-07深入淺出JS的Object.defineProperty()
這篇文章主要介紹了深入淺出JS的Object.defineProperty(),文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,感興趣的小伙伴可以參考一下2022-06-06