TypeScript 中的逆变、协变和双向协变

2021-12-27 08:36:11 浏览数 (1)

前言

为什么需要引入逆变、协变和双向协变这些概念

因为考虑到类型兼容,详情参考https://www.typescriptlang.org/docs/handbook/type-compatibility.html

在 TypeScript 中,有两种兼容性机制:子类型和赋值 (意思是理解成在子类型和赋值这种操作下才会触发兼容性,比如比较该类型是不是其子类型)

出于实际目的,类型兼容性由赋值兼容性决定,即使在implements and extends子句的情况下也是如此

基础

TypeScript中的类型兼容性可以用于确定一个类型是否可以赋值给其他类型。这里要了解两个概念:

官方文档说到TS 是结构性的类型系统(Type compatibility in TypeScript is based on structural subtyping. Structural typing is a way of relating types based solely on their members. This is in contrast with nominal typing. Consider the following code)

  • 结构类型:一种只使用其成员来描述类型的方式(类型 ducking type);
  • 名义类型:明确的指出或声明其类型,如c#,java。

TypeScript的类型兼容性就是基于结构子类型的。下面的例子:

代码语言:javascript复制
interface IName {
    name: string;
}

class Man {
    name: string;
    constructor() {
        this.name = "鸣人";
    }
}

let p: IName;
p = new Man();
p.name;

上面的代码在TypeScript不会出错,但是在java等语言中就会报错,因为Man类没有明确的说明实现了IName 接口

结构化

在基于名义类型的类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。而结构性类型系统是基于类型的组成结构,且不要求明确地声明。

TS 是结构性的类型系统所谓结构化就是对值所具有的结构进行类型检查。简单来说,要判断两个类型是否是兼容的,只需要看两个类型的结构是否兼容就可以了,不需要关心类型的名称是否相同。比如:

代码语言:javascript复制
interface Pet {
  name: string;
}
class Dog {
  name: string;
}
let pet: Pet;
// OK, because of structural typing
pet = new Dog();

子类型

比如考虑如下接口:

代码语言:javascript复制
interface Animal {
  age: number
}

interface Dog extends Animal {
  bark(): void
}

在这个例子中,Animal 是 Dog 的父类,Dog是Animal的子类型,子类型的属性比父类型更多,更具体。

在类型系统中,属性更多的类型是子类型。

在集合论中,属性更少的集合是子集。

也就是说,子类型是父类型的超集,而父类型是子类型的子集,这是直觉上容易搞混的一点。

记住一个特征,子类型比父类型更加具体,这点很关键。

可赋值性 assignable

assignable 是类型系统中很重要的一个概念,当你把一个变量赋值给另一个变量时,就要检查这两个变量的类型之间是否可以相互赋值。

代码语言:javascript复制
let animal: Animal
let dog: Dog

animal = dog //  ✅ok
dog = animal //  ❌error! animal 实例上缺少属性 'bark'

协变和逆变

如何处理类型兼容呢?通过协变和逆变原则

协变与逆变(covariance and contravariance)是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。

维基百科上关于协变和逆变的解释有点晦涩难懂。这里,我们用更通俗一点的语言来表述:

  • 协变:允许子类型转换为父类型(可以里式替换LSP原则进行理解)
  • 逆变:允许父类型转换为子类型

逆变

代码语言:javascript复制
// Dog ≼ Animal

var feedAnimal = (o: Animal) => {};
var feedDog = (o: Dog) => {
  o.bark();
};
feedDog = feedAnimal; // 成立,feedAnimal ≼ feedDog
feedAnimal = feedDog; // 严格模式下报错,因为可能animal并不能保证存在bark()

// 也就是存在如下场景
function func(f: typeof feedDog) {
  var d: Dog;
  f(d);
}
func(feedAnimal);

在函数的参数类型中,是符合逆变的函数的关系和参数的关系是相反的。但在TS中,参数类型是双向协变的(详见下文3.1小节),如果项目里开启了"strict": true,意味着,会来带开启 strictFunctionType ,此时,才按照逆变处理

双向协变

在老版本的 TS 中,函数参数是双向协变的。也就是说,既可以协变又可以逆变,但是这并不是类型安全的。在新版本 TS (2.6 ) 中 ,你可以通过开启 strictFunctionTypes 或 strict 来修复这个问题。设置之后,函数参数就不再是双向协变的了。

参考资料

  • https://juejin.cn/post/7019565189624250404
  • https://juejin.cn/post/6950254535298252836
  • why-are-functions-with-fewer-parameters-assignable-to-functions-that-take-more-parameters
  • what are covariance and contravariance

0 人点赞