TypeScript 類型兼容(逆變、協(xié)變、雙向協(xié)變和不變)的實現(xiàn)
在 TypeScript 中,類型系統(tǒng)支持“逆變(Contravariance)”、“協(xié)變(Covariance)”、“雙向協(xié)變(Bivariance)”和“不變(Invariance)”的概念,這些概念主要用于理解類型之間的兼容性,尤其是在函數(shù)參數(shù)和返回值之間的關(guān)系。
類型安全和型變
首先,TypeScript 通過在 JavaScript 上添加靜態(tài)類型系統(tǒng)來實現(xiàn)類型安全。類型安全意味著我們可以在編譯時檢測到可能的類型錯誤,防止它們在運行時導(dǎo)致程序崩潰。例如,TypeScript 不允許將一個 number 類型的值賦給一個 boolean 類型的變量,也不允許調(diào)用某個對象上不存在的方法。
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'.
在這兩個例子中,TypeScript 的類型檢查機制會在編譯時報告錯誤,從而保證了類型的正確性和代碼的可靠性。
然而,完全嚴格的類型安全有時會導(dǎo)致不便。例如,當我們處理子類型和父類型時,TypeScript 允許一些 變通,以便在保證類型安全的前提下提供更大的靈活性。這種 變通 被稱為型變(Variance)。
在類型系統(tǒng)中,當類型 A 和類型 B 存在繼承關(guān)系時,使用 A 的地方是否也能使用 B。型變在函數(shù)的參數(shù)和返回值中表現(xiàn)尤為明顯。型變通常包括以下四種類型:
- 協(xié)變(Covariance):如果 A 是 B 的子類型,那么
F<A>也是F<B>的子類型。換句話說,F<A>可以賦值給F<B>。協(xié)變通常用于函數(shù)的返回值類型。 - 逆變(Contravariance):如果 A 是 B 的子類型,那么
F<B>是F<A>的子類型。換句話說,F<B>可以賦值給F<A>。逆變通常用于函數(shù)的參數(shù)類型。
雙向協(xié)變(Bivariance):TypeScript 中的一種特殊情況,允許函數(shù)參數(shù)類型既可以協(xié)變也可以逆變,雖然這種情況理論上不安全,但它是為了保持實際代碼的兼容性。
不變(Invariance):F<A> 既不是 F<B> 的子類型,也不是其超類型。換句話說,F<A> 和 F<B> 是完全不兼容的類型。
協(xié)變
在 TypeScript 中,協(xié)變 主要是指在處理泛型、函數(shù)類型、數(shù)組等結(jié)構(gòu)時,允許子類型被視為其父類型。簡言之,協(xié)變允許子類型可以賦值給父類型,這是最常見的類型兼容性規(guī)則。
泛型默認是協(xié)變的,這就以為著如果 B 是 A 的子類型,那么 T<B> 也是 T<A> 的子類型。
例如,假設(shè)我們有一個泛型接口 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é)變
他們相互賦值是不會錯的,雖然這倆類型不一樣,但是依然是類型安全的。

在這個例子中,Box<Dog> 可以賦值給 Box<Animal>,因為 Dog 是 Animal 的子類型。這種關(guān)系在 TypeScript 的類型系統(tǒng)中是允許的,因為它符合協(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é)變在 TypeScript 中非常重要,因為它允許類型系統(tǒng)具有更大的靈活性。協(xié)變允許你在不違反類型安全的情況下,靈活地使用繼承結(jié)構(gòu)中的子類型和父類型。
在使用泛型數(shù)據(jù)結(jié)構(gòu)時,協(xié)變允許我們在不影響類型安全的情況下,處理更廣泛的類型。例如,List<Dog> 可以在 List<Animal> 的上下文中使用,因為 Dog 是 Animal 的子類型。協(xié)變使得函數(shù)可以返回更具體的類型而不影響函數(shù)的兼容性,這有助于設(shè)計靈活且類型安全的接口。例如,你可以在接口的實現(xiàn)中返回一個更具體的類型,而不需要更改接口本身的簽名。
雖然協(xié)變提供了很多靈活性,但是在某些情況下也可能會帶來問題,例如數(shù)組的協(xié)變可能會導(dǎo)致運行時錯誤,例如在使用 push 操作的時候,它可能會破壞數(shù)組的類型一致性,從而導(dǎo)致運行時報錯:
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
let animals: Animal[] = [new Dog()];
animals.push(new Cat()); // 合法,但可能不是預(yù)期的行為
在這個例子中,animals 原本是 Dog[] 類型,但由于數(shù)組的協(xié)變,Cat 實例被推入了這個數(shù)組,這可能會導(dǎo)致一些不一致的問題。
逆變
在 TypeScript 中,逆變 是指,如果類型 A 是類型 B 的父類型(A <: B),那么在函數(shù)參數(shù)的位置上,類型 B 可以賦值給類型 A。換句話說,逆變允許父類型的函數(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è)我們有兩個函數(shù)類型,一個接受 Animal 作為參數(shù),另一個接受 Dog 作為參數(shù):
type AnimalHandler = (animal: Animal) => void; type DogHandler = (dog: Dog) => void;
在逆變的情況下,AnimalHandler 可以賦值給 DogHandler。這是因為 Dog 是 Animal 的子類型,因此處理 Animal 的函數(shù)也可以處理 Dog。
let handleAnimal: AnimalHandler = (animal: Animal) => {
animal.speak();
};
let handleDog: DogHandler;
handleDog = handleAnimal; // 合法,逆變
在這個例子中,handleAnimal 可以處理任何 Animal,包括 Dog。因此,我們可以將 handleAnimal 賦值給 handleDog,這在類型系統(tǒng)中是安全的。
逆變的直觀理解是,你可以將處理較寬泛類型的函數(shù)賦值給處理更具體類型的函數(shù)。如果一個函數(shù)能夠處理 Animal,那么它肯定能夠處理 Dog,因為 Dog 是 Animal 的一種特殊化形式。
在 TypeScript 中,型變通常用于描述類型在不同上下文中的傳遞方式。逆變與協(xié)變是相對的:
- 協(xié)變:子類型可以賦值給父類型。例如,Dog 是 Animal 的子類型,那么 Dog 類型的值可以賦值給 Animal 類型的變量。
- 逆變:父類型可以賦值給子類型。例如,Animal 是 Dog 的父類型,那么接受 Animal 參數(shù)的函數(shù)可以賦值給接受 Dog 參數(shù)的函數(shù)。
在 React 中,事件處理函數(shù)是逆變的一種典型應(yīng)用。當你在一個通用的事件處理器中使用更廣泛的事件類型時,逆變可以確保你的事件處理器適用于子類型事件。
例如,考慮一個處理 MouseEvent 的事件處理器:
type MouseEventHandler = (event: React.MouseEvent<HTMLButtonElement>) => void;
如果我們有一個更通用的事件處理函數(shù),它可以處理任意的 DOM 事件,那么它可以被安全地用作處理特定的 MouseEvent:
type AnyEventHandler = (event: React.SyntheticEvent) => void;
const handleEvent: AnyEventHandler = (event) => {
console.log(event);
};
const handleMouseEvent: MouseEventHandler = handleEvent; // 合法,逆變
在這個例子中,handleEvent 可以處理任何 SyntheticEvent,其中包括 MouseEvent,所以它可以被賦值給 handleMouseEvent。這就是逆變的一個實際應(yīng)用:更廣泛的處理函數(shù)可以用于處理更具體的事件類型。
在使用高階組件時,逆變也可能發(fā)揮作用。高階組件(HOC)是接受一個組件并返回一個新組件的函數(shù)。在處理高階組件時,如果傳遞給 HOC 的組件的屬性是某種類型的父類型,那么 HOC 返回的組件可以安全地接受更具體的子類型屬性。
假設(shè)你有一個高階組件 withLogging,它可以將日志功能添加到任何組件中:
function withLogging<P>(Component: React.ComponentType<P>): React.FC<P> {
return (props: P) => {
console.log("Rendering", Component.name);
return <Component {...props} />;
};
}
如果你有一個組件 DogComponent 接受 Dog 類型的屬性,那么你可以將 withLogging 應(yīng)用于 DogComponent,并返回一個同樣接受 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 的一個具體實例。由于 TypeScript 支持逆變,因此即使 P 是父類型,它也可以接受子類型 Dog 的參數(shù),這使得 LoggedDogComponent 可以安全地使用 Dog 類型的屬性。
逆變 是 TypeScript 中的一種型變,允許父類型的函數(shù)參數(shù)賦值給子類型的函數(shù)參數(shù)。它保證了類型安全,因為處理父類型的函數(shù)可以適用于子類型,而不會引發(fā)類型錯誤。這通常用于函數(shù)參數(shù)類型的兼容性處理,確保函數(shù)的靈活性和擴展性。
雙向協(xié)變
在類型系統(tǒng)中,協(xié)變允許子類型賦值給父類型,逆變則允許父類型賦值給子類型。雙向協(xié)變是 TypeScript 中的一種特殊行為,它允許函數(shù)參數(shù)的類型既可以協(xié)變,也可以逆變。
簡單來說,雙向協(xié)變允許你在處理函數(shù)參數(shù)類型時,既可以將子類型賦值給父類型,也可以將父類型賦值給子類型。
假設(shè)我們有兩個類 Animal 和 Dog,Dog 繼承自 Animal:
class Animal {
speak() {
console.log("Animal sound");
}
}
class Dog extends Animal {
speak() {
console.log("Bark");
}
}
現(xiàn)在我們定義兩個函數(shù)類型,一個接受 Dog 作為參數(shù),另一個接受 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; // 合法,逆變
在這個例子中,handleAnimal 可以處理 Animal 類型的參數(shù),而 handleDog 只處理 Dog 類型的參數(shù)。盡管從理論上來說,將 handleAnimal 賦值給 handleDog 可能存在類型安全問題(handleAnimal 可能接受 Cat,而 handleDog 只能處理 Dog),但 TypeScript 允許這種賦值操作。
考慮一個更復(fù)雜的例子,其中我們有一個函數(shù)接受另一個函數(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ā)類型安全問題。特別是在處理繼承關(guān)系較為復(fù)雜的情況下,可能會導(dǎo)致運行時錯誤。
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()); // 運行時錯誤,因為 Animal 不是 Dog
在這個例子中,將 handleAnyAnimal 賦值給 dogHandler 是合法的,因為 TypeScript 允許這種雙向協(xié)變。然而,當我們嘗試傳遞一個 Animal(而不是 Dog)給 dogHandler 時,可能會引發(fā)運行時錯誤。
不變
在類型系統(tǒng)中,不變 是指某個類型不能在子類型和父類型之間相互替換。具體來說,如果你有一個泛型類型 T<A> 和 T<B>,即使 A 是 B 的子類型,T<A> 也不能賦值給 T<B>,反之亦然。這種嚴格的類型匹配規(guī)則被稱為不變。
考慮一個簡單的泛型類:
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; // 錯誤,Box<Dog> 不能賦值給 Box<Animal> dogBox = animalBox; // 錯誤,Box<Animal> 不能賦值給 Box<Dog>
這就是不變的體現(xiàn)。TypeScript 要求 Box<Animal> 和 Box<Dog> 必須是完全一致的類型,任何嘗試在這些類型之間進行賦值都會導(dǎo)致類型錯誤。
不變是一種型變規(guī)則,要求類型在所有上下文中必須嚴格匹配。比如我想要一只鴨子,你不能給我一個碗對吧,碗又不能吃。
總結(jié)
本文詳細介紹了 TypeScript 類型系統(tǒng)中的四種型變概念:協(xié)變、逆變、雙向協(xié)變 和 不變,以及它們在類型安全和靈活性方面的作用。
- 協(xié)變:允許子類型賦值給父類型,常見于函數(shù)的返回值和數(shù)組類型。協(xié)變使得類型系統(tǒng)更加靈活,例如,
List<Dog>可以在需要List<Animal>的地方使用。 - 逆變:允許父類型賦值給子類型,常見于函數(shù)的參數(shù)類型。逆變確保了類型系統(tǒng)的靈活性和安全性,比如將處理
Animal的函數(shù)賦值給處理Dog的函數(shù)。 - 雙向協(xié)變:一種特殊情況,允許函數(shù)參數(shù)類型既可以協(xié)變也可以逆變,盡管這可能引發(fā)類型安全問題。雙向協(xié)變在 TypeScript 中存在主要是為了保持與 JavaScript 的兼容性。
- 不變:要求類型在所有上下文中嚴格匹配,不能在子類型和父類型之間相互替換。這種嚴格的類型檢查確保了類型系統(tǒng)的安全性,但也減少了靈活性。
理解這些型變概念能夠幫助開發(fā)者在 TypeScript 中編寫既靈活又安全的代碼,尤其是在處理復(fù)雜的類型關(guān)系和函數(shù)參數(shù)時。它們?yōu)槲覀兲峁┝艘粋€平衡類型安全性和代碼靈活性的工具。
到此這篇關(guān)于TypeScript 類型兼容(逆變、協(xié)變、雙向協(xié)變和不變)的實現(xiàn)的文章就介紹到這了,更多相關(guān)TypeScript 類型兼容內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javascript HTML5 canvas實現(xiàn)打磚塊游戲
這篇文章主要介紹了基于javascript HTML5 canvas實現(xiàn)打磚塊游戲的具體實現(xiàn)代碼,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-04-04
JavaScript關(guān)于prototype實例詳解(超重點)
prototype是js里面給類增加功能擴展的一種模式,這篇文章主要介紹了JavaScript關(guān)于prototype(超重點),本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-08-08
微信小程序使用slider設(shè)置數(shù)據(jù)值及switch開關(guān)組件功能【附源碼下載】
這篇文章主要介紹了微信小程序使用slider設(shè)置數(shù)據(jù)值及switch開關(guān)組件功能,結(jié)合實例形式分析了slider組件及switch組件的功能與使用方法,并附帶源碼供讀者下載參考,需要的朋友可以參考下2017-12-12
JavaScript關(guān)于提高網(wǎng)站性能的幾點建議(一)
這篇文章主要介紹了JavaScript關(guān)于提高網(wǎng)站性能的幾點建議(一)的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-07-07
深入淺出JS的Object.defineProperty()
這篇文章主要介紹了深入淺出JS的Object.defineProperty(),文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,感興趣的小伙伴可以參考一下2022-06-06

