在 TypeScript 中,类型系统支持逆变、协变、双向协变和不变等概念。这些概念是理解 TypeScript 类型系统时必须掌握的基础,尤其在函数参数类型和返回值类型的兼容性问题上,起着至关重要的作用。
类型安全和型变
在 TypeScript 中,类型系统通过在 JavaScript 上添加静态类型来实现类型安全。类型安全意味着可以在编译时发现潜在的类型错误,从而避免它们在运行时导致程序崩溃。例如,TypeScript 不允许将一个 number
类型的值赋给 boolean
类型的变量,也不允许调用某个对象上不存在的方法。
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 的类型检查机制会在编译时报告错误,从而确保类型的正确性和代码的可靠性。
然而,严格的类型安全有时可能会带来不便,特别是在处理子类型和父类型之间的关系时。为了在保证类型安全的同时提供更多灵活性,TypeScript 引入了类型变换(Variance)的概念。
类型变换描述了当类型 A 和类型 B 存在继承关系时,是否可以在使用 A 的地方使用 B。类型变换在函数参数类型和返回值类型中尤为重要。常见的类型变换包括以下几种:
-
协变(Covariance):如果 A 是 B 的子类型,那么
F<A>
也是F<B>
的子类型。换句话说,F<A>
可以赋值给F<B>
。协变通常用于函数的返回值类型。例如,返回类型的类型变换一般采用协变。 -
逆变(Contravariance):如果 A 是 B 的子类型,那么
F<B>
是F<A>
的子类型。换句话说,F<B>
可以赋值给F<A>
。逆变通常用于函数的参数类型。函数的参数类型往往会遵循逆变规则。 -
双向协变(Bivariance):TypeScript 中的一种特殊情况,允许函数的参数类型同时表现出协变和逆变的特性。虽然这种情况理论上不安全,但它为保持代码兼容性提供了方便。
-
不变(Invariance):
F<A>
既不是F<B>
的子类型,也不是其超类型。换句话说,F<A>
和F<B>
是完全不兼容的类型。在某些情况下,类型系统会强制要求类型不变,防止潜在的类型冲突。
通过理解这些类型变换规则,开发者可以更灵活地设计类型,确保代码既类型安全又具有良好的兼容性。
协变
在 TypeScript 中,协变主要指在处理泛型、函数类型、数组等结构时,它允许子类型被视为其父类型。简单来说,如果类型 A 是类型 B 的子类型,那么在使用泛型、函数返回值、数组等结构时,T<A>
也可以被视为 T<B>
的子类型。
泛型中的协变
在 TypeScript 中,泛型默认是协变的。这意味着如果 B 是 A 的子类型,那么 T<B>
也是 T<A>
的子类型。
class Animal {}
class Dog extends Animal {}
interface Box<T> {
value: T;
}
let animalBox: Box<Animal>;
let dogBox: Box<Dog> = { value: new Dog() };
animalBox = dogBox; // 合法,因为协变允许这种赋值
在上面的例子中,Box<Dog>
可以赋值给 Box<Animal>
,因为 Dog 是 Animal 的子类型。这种赋值是类型安全的,因为任何期望 Animal 的地方都可以接受 Dog。
函数返回值的协变
函数的返回值类型也遵循协变规则:
class Animal {}
class Dog extends Animal {}
type AnimalFactory = () => Animal;
type DogFactory = () => Dog;
let createAnimal: AnimalFactory;
let createDog: DogFactory = () => new Dog();
createAnimal = createDog; // 合法,返回值类型是协变的
这里,DogFactory
可以赋值给 AnimalFactory
,因为 Dog 是 Animal 的子类型。这种赋值是安全的,因为任何期望接收 Animal 的代码也可以接收 Dog。
数组的协变
数组在 TypeScript 中也是协变的:
class Animal {}
class Dog extends Animal {}
let dogs: Dog[] = [new Dog(), new Dog()];
let animals: Animal[];
animals = dogs; // 合法,数组是协变的
这允许我们将 Dog[]
类型的数组赋值给 Animal[]
类型的变量。
协变的优势与注意事项
协变为 TypeScript 的类型系统提供了更大的灵活性,使我们能够:
-
更灵活地使用继承结构:可以在需要父类型的地方使用子类型,而不违反类型安全。
-
设计更通用的接口:可以创建接受更广泛类型的接口,同时保持类型安全。
-
返回更具体的类型:在实现接口时,可以返回比接口要求更具体的类型,而不需要修改接口签名。
然而,协变也可能带来一些潜在问题,特别是在可变数据结构(如数组)中:
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
// 由于协变,这个赋值是合法的
let animals: Animal[] = [new Dog()];
// 但这可能导致运行时类型不一致
animals.push(new Cat()); // 现在数组中同时包含 Dog 和 Cat
在上面的例子中,原本只包含 Dog 实例的数组现在也包含了 Cat 实例。如果后续代码假设数组中只有 Dog 实例,可能会导致运行时错误。
小结
协变是 TypeScript 类型系统的一个重要特性,它允许子类型在特定上下文中被视为父类型,从而提供更大的类型灵活性。理解协变对于有效利用 TypeScript 的类型系统至关重要,但也需要注意可能带来的潜在问题,特别是在处理可变数据结构时。
逆变
逆变(Contravariance)是 TypeScript 类型系统中与协变相对的概念。在逆变关系中,如果类型 A 是类型 B 的子类型,那么包含 B 的复合类型反而是包含 A 的复合类型的子类型。这种关系主要体现在函数参数类型上。
函数参数中的逆变
在函数参数位置上,TypeScript 的类型系统表现出逆变特性。这意味着,如果 A 是 B 的子类型,那么接受 B 类型参数的函数可以赋值给接受 A 类型参数的函数。
下面是一个完整的代码示例:
// 定义基类 Animal
class Animal {
feed(): void {
console.log('Feeding an animal');
}
}
// 定义子类 Dog,继承自 Animal
class Dog extends Animal {
bark(): void {
console.log('Woof!');
}
}
// 定义两个函数类型
type AnimalHandler = (animal: Animal) => void;
type DogHandler = (dog: Dog) => void;
// 创建一个处理 Animal 的函数
let handleAnimal: AnimalHandler = (animal: Animal) => {
animal.feed();
};
// 创建一个处理 Dog 的函数
let handleDog: DogHandler = (dog: Dog) => {
dog.feed();
dog.bark();
};
// 逆变允许将处理父类型的函数赋值给处理子类型的函数
let dogHandler: DogHandler;
dogHandler = handleAnimal; // 合法,这是逆变
// 使用 dogHandler 处理 Dog 实例
const myDog = new Dog();
dogHandler(myDog); // 实际上调用的是 handleAnimal,这是安全的
// 以下赋值在严格模式下不合法
// handleAnimal = handleDog; // 错误:不能将 DogHandler 赋值给 AnimalHandler
// 因为 handleDog 可能会使用 Dog 特有的方法(如 bark),而这些方法在 Animal 上不存在
// 创建另一个子类 Cat
class Cat extends Animal {
meow(): void {
console.log('Meow!');
}
}
// 如果允许上面的赋值,会导致以下问题:
// const myCat = new Cat();
// handleAnimal(myCat); // 如果 handleAnimal 实际上是 handleDog,这里会尝试调用 bark 方法,但 Cat 没有这个方法
这种赋值是类型安全的,因为 handleAnimal
只会使用 Animal 类型上存在的方法(如 feed()
),而这些方法在 Dog 类型上也一定存在。
逆变的直观理解
逆变可以通过”能力”的角度来理解:
- 如果一个函数能够处理所有动物,那么它自然也能处理狗(因为狗是动物的一种)。
- 因此,接受 Animal 参数的函数可以安全地用在需要接受 Dog 参数的地方。
换句话说,逆变允许我们将处理”更宽泛类型”的函数赋值给处理”更具体类型”的函数。
在前端开发中,事件处理是逆变的典型应用场景:
// 一个可以处理任何 DOM 事件的处理器
type GenericEventHandler = (event: Event) => void;
// 一个专门处理鼠标事件的处理器
type MouseEventHandler = (event: MouseEvent) => void;
const handleAnyEvent: GenericEventHandler = (event) => {
console.log('Event occurred:', event.type);
};
// 逆变允许这种赋值
const handleMouseEvent: MouseEventHandler = handleAnyEvent; // 合法
这里,handleAnyEvent
可以处理任何 Event(包括 MouseEvent),所以它可以安全地用作 MouseEvent 的处理器。
依赖注入
在依赖注入模式中,逆变也很有用:
interface Logger {
log(message: string): void;
}
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(message);
}
}
// 接受任何 Logger 的服务
class UserService {
constructor(private logger: Logger) {}
createUser(name: string): void {
this.logger.log(`Creating user: ${name}`);
// ...
}
}
// 可以传入更具体的 ConsoleLogger
const service = new UserService(new ConsoleLogger());
逆变与协变的对比
理解逆变和协变的区别对于掌握 TypeScript 的类型系统至关重要:
特性 | 协变 | 逆变 |
---|---|---|
定义 | 如果 A 是 B 的子类型,则 T<A> 是 T<B> 的子类型 | 如果 A 是 B 的子类型,则 T<B> 是 T<A> 的子类型 |
应用场景 | 函数返回值、数组、泛型类 | 函数参数 |
方向 | 保持子类型关系的方向 | 反转子类型关系的方向 |
安全性 | 在读取操作中安全 | 在写入操作中安全 |
小结
逆变是 TypeScript 类型系统中的一个重要概念,它主要应用于函数参数类型的兼容性判断。逆变允许将处理更宽泛类型的函数赋值给处理更具体类型的函数,这种特性在事件处理、依赖注入等场景中非常有用。理解逆变与协变的区别和应用场景,有助于更好地利用 TypeScript 的类型系统,编写类型安全且灵活的代码。
双向协变
双向协变(Bivariance)是 TypeScript 类型系统中的一个特殊概念,它允许函数参数类型同时具有协变和逆变的特性。简单来说,双向协变使得函数参数类型既可以接受子类型赋值给父类型(协变),也可以接受父类型赋值给子类型(逆变)。
双向协变的定义与行为
在严格的类型理论中,函数参数应该是逆变的,而函数返回值应该是协变的。然而,为了提高灵活性和兼容性,TypeScript 在某些情况下允许函数参数表现出双向协变的特性。
下面是一个完整的代码示例,展示了双向协变的行为:
// 定义基类和子类
class Animal {
feed(): void {
console.log('Feeding an animal');
}
}
class Dog extends Animal {
bark(): void {
console.log('Woof!');
}
}
// 定义两个函数类型
type DogHandler = (dog: Dog) => void;
type AnimalHandler = (animal: Animal) => void;
// 创建函数实例
let handleDog: DogHandler = (dog: Dog) => {
dog.feed();
dog.bark();
};
let handleAnimal: AnimalHandler = (animal: Animal) => {
animal.feed();
// 注意:这里不能调用 bark(),因为 Animal 没有这个方法
};
// 双向协变允许以下两种赋值
// 1. 将处理子类型的函数赋值给处理父类型的函数(协变)
handleAnimal = handleDog; // 合法,协变
// 2. 将处理父类型的函数赋值给处理子类型的函数(逆变)
handleDog = handleAnimal; // 在非严格模式下合法,这是双向协变的体现
双向协变的配置
在 TypeScript 中,双向协变的行为受到 strictFunctionTypes
编译选项的影响:
- 当
strictFunctionTypes
设置为false
(默认情况下)时,TypeScript 允许函数参数类型的双向协变。 - 当
strictFunctionTypes
设置为true
时,TypeScript 强制函数参数类型为逆变,不再允许双向协变。
// tsconfig.json
{
"compilerOptions": {
"strictFunctionTypes": true // 禁用双向协变,强制函数参数类型为逆变
}
}
双向协变的安全性问题
虽然双向协变提供了更大的灵活性,但它可能导致类型安全问题。考虑以下例子:
class Animal {
feed(): void {
console.log('Feeding an animal');
}
}
class Dog extends Animal {
bark(): void {
console.log('Woof!');
}
}
class Cat extends Animal {
meow(): void {
console.log('Meow!');
}
}
type AnimalHandler = (animal: Animal) => void;
type DogHandler = (dog: Dog) => void;
// 创建一个只能处理 Dog 的函数
let dogOnlyHandler: DogHandler = (dog: Dog) => {
dog.feed();
dog.bark(); // 使用了 Dog 特有的方法
};
// 在双向协变下,这个赋值是合法的
let generalHandler: AnimalHandler = dogOnlyHandler;
// 但这可能导致运行时错误
const cat = new Cat();
generalHandler(cat); // 运行时错误:cat 没有 bark 方法
在上面的例子中,dogOnlyHandler
被赋值给 generalHandler
,这在协变规则下是合法的。然而,当我们尝试用 generalHandler
处理一个 Cat
对象时,会导致运行时错误,因为 dogOnlyHandler
尝试调用 bark
方法,而 Cat
没有这个方法。
双向协变的实际应用
尽管双向协变可能带来类型安全问题,但在某些场景下它仍然很有用:
- 事件处理系统:在处理 DOM 事件时,双向协变允许更灵活地组织事件处理函数。
// 一个通用的事件处理函数
function handleEvent(callback: (event: Event) => void) {
// 处理事件...
}
// 一个特定的鼠标事件处理函数
const mouseHandler = (event: MouseEvent) => {
console.log(event.clientX, event.clientY);
};
// 双向协变允许这种调用
handleEvent(mouseHandler); // 合法
- 回调函数:在使用回调函数的 API 中,双向协变可以简化代码。
function processItems<T>(items: T[], callback: (item: T) => void) {
items.forEach(callback);
}
const numbers = [1, 2, 3];
const processNumber = (n: number | string) => {
console.log(n.toString());
};
// 双向协变允许这种调用
processItems(numbers, processNumber); // 合法
双向协变与其他型变的对比
特性 | 协变 | 逆变 | 双向协变 |
---|---|---|---|
定义 | 子类型可赋值给父类型 | 父类型可赋值给子类型 | 子类型和父类型可相互赋值 |
应用场景 | 函数返回值、数组 | 函数参数 | 函数参数(在非严格模式下) |
类型安全性 | 在读取操作中安全 | 在写入操作中安全 | 可能不安全,取决于具体使用场景 |
TypeScript 配置 | 默认启用 | 在 strictFunctionTypes: true 时强制应用于函数参数 | 在 strictFunctionTypes: false 时应用于函数参数 |
小结
双向协变是 TypeScript 类型系统中的一个特殊特性,它允许函数参数类型同时具有协变和逆变的特性。虽然双向协变提供了最大的灵活性和兼容性,但它也可能导致类型安全问题。在实际开发中,建议根据项目的需求和安全性要求,合理配置 strictFunctionTypes
选项,以在灵活性和类型安全性之间取得平衡。
不变
不变(Invariance)是类型系统中最严格的型变规则,它要求类型必须精确匹配,不允许任何形式的子类型或父类型替换。在不变的规则下,即使类型 A 是类型 B 的子类型,T<A>
和 T<B>
也被视为完全不同且不兼容的类型。
不变的定义与特性
不变可以被理解为”类型必须完全一致”的规则。在不变的类型关系中:
- 子类型不能赋值给父类型(不允许协变)
- 父类型不能赋值给子类型(不允许逆变)
- 只有完全相同的类型才能相互赋值
不变的实际应用
在 TypeScript 中,某些泛型类型被设计为不变的,以确保类型安全。下面是一个完整的示例,展示了不变的行为:
// 定义基类和子类
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
eat(): void {
console.log(`${this.name} is eating`);
}
}
class Dog extends Animal {
breed: string;
constructor(name: string, breed: string) {
super(name);
this.breed = breed;
}
bark(): void {
console.log('Woof!');
}
}
class Cat extends Animal {
color: string;
constructor(name: string, color: string) {
super(name);
this.color = color;
}
meow(): void {
console.log('Meow!');
}
}
// 定义一个不变的泛型类
class Container<T> {
private _value: T;
constructor(value: T) {
this._value = value;
}
getValue(): T {
return this._value;
}
setValue(value: T): void {
this._value = value;
}
}
// 创建不同类型的容器
const animalContainer = new Container<Animal>(new Animal('Generic Animal'));
const dogContainer = new Container<Dog>(new Dog('Rex', 'German Shepherd'));
const catContainer = new Container<Cat>(new Cat('Whiskers', 'Tabby'));
// 在不变规则下,以下赋值都是不合法的
// animalContainer = dogContainer; // 错误:Container<Dog> 不能赋值给 Container<Animal>
// dogContainer = animalContainer; // 错误:Container<Animal> 不能赋值给 Container<Dog>
// catContainer = animalContainer; // 错误:Container<Animal> 不能赋值给 Container<Cat>
在上面的例子中,尽管 Dog 和 Cat 都是 Animal 的子类型,但 Container<Dog>
和 Container<Animal>
之间不能相互赋值,这体现了不变的特性。
不变的必要性
不变在某些场景下是必要的,特别是当泛型类型同时涉及读取和写入操作时。考虑以下情况:
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
// 假设 MutableBox<T> 是协变的
class MutableBox<T> {
content: T;
setContent(value: T): void {
this.content = value;
}
getContent(): T {
return this.content;
}
}
const dogBox = new MutableBox<Dog>();
dogBox.setContent(new Dog()); // 正常
// 如果允许协变,以下代码将合法
const animalBox: MutableBox<Animal> = dogBox; // 假设这是合法的
// 但这会导致类型安全问题
animalBox.setContent(new Animal()); // 这会将一个没有 breed 属性的 Animal 放入期望存储 Dog 的容器中
const myDog: Dog = dogBox.getContent(); // 运行时错误:myDog.breed 不存在
在这个例子中,如果 MutableBox<T>
是协变的,那么 MutableBox<Dog>
可以赋值给 MutableBox<Animal>
。这看似合理,但会导致类型安全问题:我们可以通过 animalBox
将普通的 Animal 对象放入容器,然后通过 dogBox
取出并尝试访问 Dog 特有的属性,这将导致运行时错误。
不变与可变性的关系
不变通常与可变性(mutability)密切相关。当一个泛型类型允许对其泛型参数进行写入操作时,该类型通常应该是不变的,以确保类型安全。
- 只读操作:如果一个泛型类型只允许读取操作(如
Readonly<T>
),它可以安全地设计为协变的。 - 只写操作:如果一个泛型类型只允许写入操作,它可以安全地设计为逆变的。
- 读写操作:如果一个泛型类型同时允许读取和写入操作,它应该设计为不变的,以确保类型安全。
不变的直观理解
不变可以通过日常生活中的例子来理解:
如果你点了一杯咖啡,服务员不能给你一杯茶(即使茶是饮料的一种),也不能给你一杯饮料(即使咖啡是饮料的一种)。你需要的是确切的咖啡,不多不少。
同样,如果一个函数需要一个 Container<Dog>
,你不能给它一个 Container<Animal>
或 Container<Cat>
,即使 Dog 是 Animal 的子类型,Cat 是 Animal 的子类型。类型必须精确匹配。
不变与其他型变的对比
特性 | 协变 | 逆变 | 双向协变 | 不变 |
---|---|---|---|---|
定义 | 子类型可赋值给父类型 | 父类型可赋值给子类型 | 子类型和父类型可相互赋值 | 类型必须精确匹配 |
应用场景 | 函数返回值、只读容器 | 函数参数、只写容器 | 函数参数(非严格模式) | 可读写容器、严格类型匹配 |
类型安全性 | 在读取操作中安全 | 在写入操作中安全 | 可能不安全 | 最安全,但最不灵活 |
灵活性 | 中等 | 中等 | 高 | 低 |
小结
不变是 TypeScript 类型系统中最严格的型变规则,它要求类型必须精确匹配,不允许任何形式的子类型或父类型替换。虽然不变限制了类型的灵活性,但它提供了最高级别的类型安全保证,特别是在处理可读写的泛型容器时。理解不变及其应用场景,有助于设计类型安全且符合预期的 TypeScript 代码。
用有意思的例子来讲解
接下来我们将用一些更贴近生活、更有趣的例子来讲解 逆变(Contravariance)、协变(Covariance)、双向协变(Bivariance) 和 不变(Invariance),让它们更容易理解!😊
🎭 1. 协变(Covariance)—— “专业厨师和菜谱”
协变的核心概念是:可以返回更具体的类型。
🔥 场景:
想象你去了一家餐厅,你点了一道 “通用的意大利面”(Pasta
)。餐厅的主厨是一位意大利面大师(ItalianChef
),他决定给你做 松露意大利面(TrufflePasta
),而不是普通的意大利面。
🏗 代码类比:
class Pasta {}
class TrufflePasta extends Pasta {} // 松露意大利面 🍝
type CookPasta = () => Pasta;
type CookTrufflePasta = () => TrufflePasta; // 返回更具体的意大利面 🍝
let chef: CookPasta;
let italianChef: CookTrufflePasta = () => new TrufflePasta();
chef = italianChef; // ✅ 合法!协变
✅ 现实中的合理性:
顾客点了“普通意大利面”,但主厨给你上了一道更高级的“松露意大利面”——这当然没问题,因为你仍然得到了你想要的东西,而且更好了! 这就是 协变:你可以返回更具体的类型,但不能返回更抽象的类型。
🏓 2. 逆变(Contravariance)—— “宠物医生”
逆变的核心概念是:可以接受更一般的类型。
🐶 🏥 场景:
假设你是一名 宠物医生(Veterinarian),你能治疗各种动物,比如 🐶狗(Dog
) 和 🐱猫(Cat
)。
但如果你是一个只会治疗狗的医生(DogDoctor
),那么当有一只猫跑进你的诊所时,你可能就束手无策了。
🏗 代码类比:
class Animal {} // 任何动物 🦁
class Dog extends Animal {} // 具体的狗 🐶
class Cat extends Animal {} // 具体的猫 🐱
type TreatAnimal = (animal: Animal) => void; // 任何动物医生
type TreatDog = (dog: Dog) => void; // 只会治疗狗的医生
let generalVet: TreatAnimal;
let dogVet: TreatDog = (dog) => console.log('Treating a dog! 🐶');
// ✅ 逆变:可以用更宽泛的医生去替代只会看狗的医生
dogVet = generalVet; // ✅ 合法!逆变
✅ 现实中的合理性:
如果一个医生(generalVet
)可以治疗 所有动物,那么你当然可以让他治疗狗。
但如果一个医生(dogVet
)只会治疗狗,你不能让他治疗所有动物,这就不符合逻辑了!
这就是 逆变:你可以用更一般的类型去代替更具体的类型,但不能反过来。
🤹 3. 双向协变(Bivariance)—— “厨师 & 送餐员”
双向协变的核心概念是:参数既可以逆变,也可以协变(尽管不完全安全,但 TypeScript 允许)。
🍽️ 场景:
- 你有一个可以 做菜 的厨师(Cook)。
- 你有一个可以 送菜 的送餐员(DeliverFood)。
厨师和送餐员都处理 食物(Food),但送餐员不管是高级料理还是普通食物都能送。
🏗 代码类比:
class Food {} // 食物 🍽️
class Pizza extends Food {} // 比萨 🍕
type Cook = (f: Pizza) => Pizza; // 只会做披萨的厨师 🍕
type Deliver = (f: Food) => Food; // 可以送所有食物的送餐员 🚴
let chef: Cook;
let deliveryGuy: Deliver = (food) => food;
// 双向协变,TS 允许:
chef = deliveryGuy; // ✅ 允许
deliveryGuy = chef; // ✅ 也允许(尽管不完全安全)
✅ 现实中的合理性:
送餐员可以送任何食物(更广泛的类型),但他也可以送披萨(更具体的类型)。
厨师只会做披萨,但如果 TypeScript 允许他处理所有食物,那就有点危险了(可能导致不安全的类型转换)。
这就是 双向协变——TypeScript 出于实际编程的考虑,放宽了一些限制。
🚧 4. 不变(Invariance)—— “安全门禁”
不变的核心概念是:类型必须完全匹配,不能有子类型或父类型的替换。
场景:
想象一个 公司门禁系统,只有持有特定身份卡(IDCard
)的人才能进入办公室。如果你试图用 访问卡(AccessCard
) 或 贵宾卡(VIPCard
) 代替它,系统是不允许的。
🏗 代码类比:
class IDCard {} // 员工ID卡
class AccessCard extends IDCard {} // 普通访问卡
class VIPCard extends IDCard {} // 贵宾卡
interface OfficeEntry<T> {
card: T;
}
let idEntry: OfficeEntry<IDCard>;
let accessEntry: OfficeEntry<AccessCard> = { card: new AccessCard() };
// ❌ 不变性:不能将 `OfficeEntry<AccessCard>` 赋值给 `OfficeEntry<IDCard>`
idEntry = accessEntry; // ❌ 报错!
❌ 现实中的严格性:
如果门禁系统的类型是 OfficeEntry<IDCard>
,那么它就不能接受任何 AccessCard 或 VIPCard,因为它们是不同的类型。
这就是 不变性,类型必须严格匹配,不能有任何变通。
🎯 小结
型变类型 | 生活类比 | 代码示例 |
---|---|---|
协变(Covariance) | 餐厅厨师上更高级的意大利面 | 返回值类型可以是更具体的子类型 |
逆变(Contravariance) | 兽医可以看所有动物 | 参数类型可以是更一般的父类型 |
双向协变(Bivariance) | 厨师 & 送餐员,TS 允许 | 参数类型既可协变也可逆变(不完全安全) |
不变(Invariance) | 门禁系统严格匹配卡片类型 | 类型必须完全匹配,不能互相赋值 |
希望这些例子能让你更直观地理解 TypeScript 中的 协变、逆变、双向协变和不变!🎉 如果有不清楚的地方,随时问我!😆
总结
TypeScript 的类型系统通过型变规则在类型安全和灵活性之间取得平衡。本文详细探讨了四种型变概念及其在实际开发中的应用:
型变规则对比
型变类型 | 定义 | 应用场景 | 优势 | 潜在问题 |
---|---|---|---|---|
协变 | 子类型可赋值给父类型 T<Dog> 可赋值给 T<Animal> | 函数返回值、数组、只读容器 | 提高类型灵活性,允许更具体的实现 | 在可变容器中可能导致运行时类型错误 |
逆变 | 父类型可赋值给子类型 (Animal) => void 可赋值给 (Dog) => void | 函数参数、只写容器 | 允许更通用的处理函数用于特定场景 | 在非严格模式下可能被双向协变覆盖 |
双向协变 | 子类型和父类型可相互赋值 | 函数参数(非严格模式下) | 提供最大的灵活性和兼容性 | 可能导致类型安全问题 |
不变 | 类型必须精确匹配 | 可读写容器、需要严格类型匹配的场景 | 提供最高级别的类型安全保证 | 限制了类型的灵活性 |