Skip to Content

面试导航 - 程序员面试题库大全 | 前端后端面试真题 | 面试

Typescript逆变、协变、双向协变和不变

在 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。类型变换在函数参数类型和返回值类型中尤为重要。常见的类型变换包括以下几种:

  1. 协变(Covariance):如果 A 是 B 的子类型,那么 F<A> 也是 F<B> 的子类型。换句话说,F<A> 可以赋值给 F<B>。协变通常用于函数的返回值类型。例如,返回类型的类型变换一般采用协变。

  2. 逆变(Contravariance):如果 A 是 B 的子类型,那么 F<B>F<A> 的子类型。换句话说,F<B> 可以赋值给 F<A>。逆变通常用于函数的参数类型。函数的参数类型往往会遵循逆变规则。

  3. 双向协变(Bivariance):TypeScript 中的一种特殊情况,允许函数的参数类型同时表现出协变和逆变的特性。虽然这种情况理论上不安全,但它为保持代码兼容性提供了方便。

  4. 不变(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 的类型系统提供了更大的灵活性,使我们能够:

  1. 更灵活地使用继承结构:可以在需要父类型的地方使用子类型,而不违反类型安全。

  2. 设计更通用的接口:可以创建接受更广泛类型的接口,同时保持类型安全。

  3. 返回更具体的类型:在实现接口时,可以返回比接口要求更具体的类型,而不需要修改接口签名。

然而,协变也可能带来一些潜在问题,特别是在可变数据结构(如数组)中:

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 类型上也一定存在。

逆变的直观理解

逆变可以通过”能力”的角度来理解:

  1. 如果一个函数能够处理所有动物,那么它自然也能处理狗(因为狗是动物的一种)。
  2. 因此,接受 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 没有这个方法。

双向协变的实际应用

尽管双向协变可能带来类型安全问题,但在某些场景下它仍然很有用:

  1. 事件处理系统:在处理 DOM 事件时,双向协变允许更灵活地组织事件处理函数。
// 一个通用的事件处理函数 function handleEvent(callback: (event: Event) => void) { // 处理事件... } // 一个特定的鼠标事件处理函数 const mouseHandler = (event: MouseEvent) => { console.log(event.clientX, event.clientY); }; // 双向协变允许这种调用 handleEvent(mouseHandler); // 合法
  1. 回调函数:在使用回调函数的 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> 也被视为完全不同且不兼容的类型。

不变的定义与特性

不变可以被理解为”类型必须完全一致”的规则。在不变的类型关系中:

  1. 子类型不能赋值给父类型(不允许协变)
  2. 父类型不能赋值给子类型(不允许逆变)
  3. 只有完全相同的类型才能相互赋值
不变的实际应用

在 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>,那么它就不能接受任何 AccessCardVIPCard,因为它们是不同的类型。
这就是 不变性类型必须严格匹配,不能有任何变通。

🎯 小结
型变类型生活类比代码示例
协变(Covariance)餐厅厨师上更高级的意大利面返回值类型可以是更具体的子类型
逆变(Contravariance)兽医可以看所有动物参数类型可以是更一般的父类型
双向协变(Bivariance)厨师 & 送餐员,TS 允许参数类型既可协变也可逆变(不完全安全)
不变(Invariance)门禁系统严格匹配卡片类型类型必须完全匹配,不能互相赋值

希望这些例子能让你更直观地理解 TypeScript 中的 协变、逆变、双向协变和不变!🎉 如果有不清楚的地方,随时问我!😆

总结

TypeScript 的类型系统通过型变规则在类型安全和灵活性之间取得平衡。本文详细探讨了四种型变概念及其在实际开发中的应用:

型变规则对比
型变类型定义应用场景优势潜在问题
协变子类型可赋值给父类型 T<Dog> 可赋值给 T<Animal>函数返回值、数组、只读容器提高类型灵活性,允许更具体的实现在可变容器中可能导致运行时类型错误
逆变父类型可赋值给子类型 (Animal) => void 可赋值给 (Dog) => void函数参数、只写容器允许更通用的处理函数用于特定场景在非严格模式下可能被双向协变覆盖
双向协变子类型和父类型可相互赋值函数参数(非严格模式下)提供最大的灵活性和兼容性可能导致类型安全问题
不变类型必须精确匹配可读写容器、需要严格类型匹配的场景提供最高级别的类型安全保证限制了类型的灵活性
最后更新于:
Copyright © 2025Moment版权所有粤ICP备2025376666