TypeScript(十四)变体(协变与逆变) 最新推荐文章于 2023-06-27 10:01:47 发布

简介: TypeScript(十四)变体(协变与逆变)最新推荐文章于 2023-06-27 10:01:47 发布

前言

本文收录于TypeScript知识总结系列文章,欢迎指正!

第一次接触到变体这个概念是在深入理解TypeScript中,类型之间的转换称为变体或者变型,在TS中,类型之间能否互相赋值,会不会报错,安不安全这些都与变体有关。本文将带大家了解ts中的变体

在Java中,每一个类都是一个个体,比如,我们定义了一个Dog和Cat两个类,这二者的结构相同。

// Dog.java
public class Dog {
    String color;
    int age;
}
 
// Cat.java
public class Cat {
    String color;
    int age;
}
// Main.java
public class Main {
    Dog myCat = new Cat();// cannot convert from Cat to Dog
    Cat myDog = new Dog();// cannot convert from Dog to Cat
    Cat myCat2 = new Cat();// 允许
    Dog myDog2 = new Dog();// 允许
}

此时使用控制变量对其二者进行实例化,可以看到,虽然Dog和Cat的属性相同,都有color以及age属性,但是不能互相声明类型及赋

"鸭子类型"

鸭子类型(duck typing)是一种动态类型机制的编程概念,它指的是一个对象的类型由它能干什么决定,而不是它属于什么类决定,它在运行时就会对对象的类型进行判断。鸭子类型的思想是:如果它看起来像鸭子,游起来像鸭子,叫起来像鸭子,那么它就是鸭子。

TS中的类型检查就是鸭子类型系统,如果两个对象类型拥有相同的属性或方法,那么它们就被认定为是相同类型

我们在TS中实现上面的效果

class Dog {
    color: string
    age: number
}
class Cat {
    color: string
    age: number
}
 
const dog: Cat = new Dog()// 允许
const cat: Dog = new Cat()// 允许

可以看到,TypeScript和JAVA的模式不同,只要结构相同就可以重复使用类型,这与其他语言不太一样。

子类型化

在学习数学的集合时,一个集合A是另一个集合B的子集,那么A可以被视为B的一个子类型;正方形可以视作矩形的一种子类,因为它继承了矩形的性质,同时又在矩形的基础上增加了一些附加的约束条件

定义

在计算机科学中,我们常说的类的继承是对父类的拓展,而子类型化(Subtyping)是父类型的拓展,它是指一种类型(子类型)是另一种类型(超类型)的一种特殊形式。与继承相同,它可以添加或覆盖超类型的属性和方法。它是较为抽象的拓展方式,没有拓展具体的值,而是拓展类型。超类型经过子类型化操作后的类型产物称为子类型。

特点

赋值兼容性

如果IDog是IAnimal的子类型,那么IDog类型的变量可以赋值给IAnimal类型的变量,举个例子说说

interface IAnimal {
    name: string
}
interface IDog extends IAnimal {
    color: string
}
const animal: IAnimal = {
    name: "阿黄"
}
const dog: IDog = Object.assign(animal, {
    color: "black"
})
const _animal: IAnimal = dog // 可以执行

其中IDog继承自IAnimal,如果使用变量将这两种类型实现,则可以将子类型的变量赋予给父类型的变量。

反身性

任何类型都是它自己的子类型。

interface IAnimal {
    name: string
}
const animal: IAnimal = {
    name: "阿黄"
}
const _animal: IAnimal = animal

传递性

如果IDog是IAnimal的子类型,IWhiteDog是IDog的子类型,那么IWhiteDog也是IAnimal的子类型

interface IAnimal {
    name: string
}
interface IDog extends IAnimal {
    color: string
}
interface IWhiteDog extends IDog {
    isWhite: boolean
}
const animal: IAnimal = {
    name: "阿黄"
}
const dog: IDog = Object.assign(animal, {
    color: "black"
})
const whiteDog: IWhiteDog = Object.assign(dog, {
    isWhite: true
})
const _animal: IAnimal = whiteDog // 可以执行

除了上述几点外,子类型还有变量协变性及函数参数逆变性的特点,我们在下文中展开讲讲

协变

如果理解了上面的鸭子类型和子类型化的概念,协变(Covariance)就不难理解。它是一种类型的转换,就像子类型化中的例子,如果类型IDog是类型IAnimal的子类型,那么dog可以赋值给animal,这个过程就是协变,即协变多变少(子类型赋值给父类型)

为了更好理解上面的例子以及后续的案例,我们写一个工具类型IsExtends,用于判断两个类型之间是否是类型的继承关系

type IsExtends<Son, Parent> = Son extends Parent ? true : false;
type animalExtendsDog = IsExtends<IAnimal, IDog>// false
type dogExtendsAnimal = IsExtends<IDog, IAnimal>// true

逆变

逆变(Contravariance)与协变相反,如果将父类赋值给子类成立,则称为逆变。我们将上述的例子修改一下变成以下代码,就是逆变的过程,即逆变少变多(父类型赋值给子类型)

const _dog: IDog = animal // 无法执行,类型 "IAnimal" 中缺少属性 "color",但类型 "IDog" 中需要该属性。

然而,一般情况下上面这段代码是会抛错的,提示缺少属性,此时我们可以借助类型断言进行转换

const _dog: IDog = animal as IDog // 可以执行

但是这么写不太安全,如果animal中缺少IDog的属性可能会抛错

除此之外,函数的参数具有逆变性(不进行类型检查就具有双变特征),我们可以借助TS的函数进行转变,首先我们写个工具类型ToFun,将类型作为参数传入函数中

type ToFun<P> = (params: P) => void

然后我们将之前两个类型IAnimal和IDog传入函数中就会发现结果与前文截然相反

type IsExtends<Son, Parent> = Son extends Parent ? true : false;
type ToFun<P> = (params: P) => void
 
type animalExtendsDogFn = IsExtends<ToFun<IAnimal>, ToFun<IDog>>// true
type dogExtendsAnimalFn = IsExtends<ToFun<IDog>, ToFun<IAnimal>>// false

tips:如果两个都是true,可以将tsconfig中的strictFunctionTypes或strict打开,此时会检测函数参数的变体

此时,我们可以将变量转换为函数的形式,达到逆变的效果

const animalFn: (animal: IAnimal) => void = (animal) => { }
const _dog: ToFun<IDog> = animalFn // 可以执行

双变

双变(Bivariance)在许多地方被称为是双向协变,个人认为不太准确,双变是指类型同时具有协变和逆变的性质,称为协变与逆变可能比较合适。我们将tsconfig中的strictFunctionTypes与strict关闭,上面的例子就会显示两个true

type IsExtends<Son, Parent> = Son extends Parent ? true : false;
type ToFun<P> = (params: P) => void
type animalExtendsDogFn = IsExtends<ToFun<IAnimal>, ToFun<IDog>>// true
type dogExtendsAnimalFn = IsExtends<ToFun<IDog>, ToFun<IAnimal>>// true

我们使用变量试试

const animalFn: (animal: IAnimal) => void = (animal) => { }
const dogFn: (dog: IDog) => void = (dog) => { }
const _dog: ToFun<IDog> = animalFn // 可以执行
const _animal: ToFun<IAnimal> = dogFn // 可以执行

不变

不变(Invariance)的概念就比较简单了,两种类型既不会发生协变,也不会逆变,完全没有关系的两种类型

interface IAnimal {
    name: string
}
interface IDog {
    color: string
}
 
let animal: IAnimal = {
    name: "阿黄"
}
let dog: IDog = {
    color: "black"
}
 
dog = animal// 不能执行,缺少对应属性
animal = dog// 不能执行,缺少对应属性

思考

看个例子

思考这个的例子,如果使用上面讲到的协变与逆变的概念应该不难理解。变量(返回值)协变,参数逆变

我们把原文的例子使用TS实现一下

首先新建三个类型,分别是IAnimal,IDog,IWhiteDog,从动物到白狗层层继承

interface IAnimal {
    name: string
}
interface IDog extends IAnimal {
    type: string
}
interface IWhiteDog extends IDog {
    isWhite: boolean
}

接着我们使用工具类型实现一个函数创建,以及之前判断继承的工具

type Fun<P, R> = (params: P) => R// 创建以P为参数,R为返回值的函数
type IsExtends<Son, Parent> = Son extends Parent ? true : false;// 是否是继承关系

最后使用上述两种工具对类型进行检测:除了自身外,当函数的参数是IAnimal,返回值是IWhiteDog类型时,此函数是IDogFn的子类型

type IDogFn = Fun<IDog, IDog> // IDog, IDog
 
type IAnimalWhiteDogFn = Fun<IAnimal, IWhiteDog>// IAnimal, IWhiteDog
type IAnimalFn = Fun<IAnimal, IAnimal>// IAnimal, IAnimal
type IWhiteDogFn = Fun<IWhiteDog, IWhiteDog>// IWhiteDog, IWhiteDog
type IWhiteDogAnimalFn = Fun<IWhiteDog, IAnimal>// IWhiteDog, IAnimal
 
type IsExtendsAnimalWhiteDog = IsExtends<IAnimalWhiteDogFn, IDogFn>// true
type IsExtendsAnimal = IsExtends<IAnimalFn, IDogFn>// false
type IsExtendsWhiteDog = IsExtends<IWhiteDogFn, IDogFn>// false
type IsExtendsWhiteDogAnimal = IsExtends<IWhiteDogAnimalFn, IDogFn>// false

函数返回值和变量一样是协变的,所以子类遵循常规的继承取IWhiteDog;参数是逆变的,因此与正常的继承行为相反,取IAnimal

原因是什么?

返回值

返回值是协变的这点不难理解,函数执行时结果是RHS(Right Hand Side)右操作数,即函数返回值同样是赋值给变量的,我们还是使用上文赋值兼容性的例子做一点修改,在外层加个函数

interface IAnimal {
    name: string
}
interface IDog extends IAnimal {
    color: string
}
const animal = () => ({
    name: "阿黄"
})
const dog = () => Object.assign(animal, {
    color: "black"
})
const _animal: typeof animal = dog // 可以执行

参数

传入的参数访问子类型中的属性是不安全的。我们针对参数的子类型举个例子,首先我们增加一个IBlackDog类型,并将IDogFn以及其他的变量实现

interface IAnimal {
    name: string
}
interface IDog extends IAnimal {
    type: string
}
interface IWhiteDog extends IDog {
    isWhite: boolean
}
interface IBlackDog extends IDog {
    isBlack: boolean
}
 
type Fun<P, R> = (params: P) => R// 函数
type IDogFn = Fun<IDog, IDog> // dog函数
const animal: IAnimal = {
    name: "阿黄"
}
const dog: IDog = Object.assign(animal, {
    type: "dog"
})
const whiteDog: IWhiteDog = Object.assign(dog, {
    isWhite: true
})
const blackDog: IBlackDog = Object.assign(dog, {
    isBlack: true
})

接着创建一个函数参数接收一个函数,这个函数结构是Fun<IDog, IDog>。此时由下面的代码可以推导出,为何协变的参数是不安全的,可能有点绕

const example = (_fn: IDogFn): void => {
    _fn(blackDog)// _fn参数限制了IDog类型,所以实参可以传递blackDog,whiteDog,dog。这里我们传入blackDog
}
example(whiteDogFn)// 抛错,参数“_whiteDog”和“params” 的类型不兼容。
 
const whiteDogFn = (_whiteDog: IWhiteDog) => {
    _whiteDog.isWhite = false// 形参取IWhiteDog,但是实参传了blackDog,此时就会抛错,找不到isWhite,因为blackDog只有isBlack。所以使用形参使用IWhiteDog是不安全的,必须传递IDog类型,或者IAnimal,因为IAnimal有的属性,IDog都有
    return dog
}
const animalFn = (_animal: IAnimal) => {
    return dog
}
example(animalFn)// 允许执行

针对上面的代码做个解释:

我们定义了一个函数whiteDogFn,接收一个参数IWhiteDog,此时我们可以直接调用IWhiteDog中的属性isWhite,到这里都还算正常。但是接下来我们将这个函数代入到example函数中,为什么会抛错?因为IDogFn类型限制我们传入blackDog,whiteDog,dog这三种类型,如果此时我们传入blackDog则会有问题,因为blackDog没有isWhite这个属性。怎么做才能解决这个问题呢?从源头上控制函数的参数类型,即使用逆变的方式限制whiteDogFn函数的参数,限制为IAnimal,此时IAnimal只提供name这个属性,我们只能调用这个属性,并且这个属性是IAnimal,IDog,IWhiteDog,IBlackDog这四个类型都有的,此时使用该函数程序就是安全的

总结

以上就是文章全部内容了,本文详细讲述了TS中的变体概念,深入讲解了子类型化操作,协变,逆变,双变,不变的概念,其中协变特点是多变少,逆变则是少变多,双变即集成协变与逆变,不变则双向都无法赋值,最后介绍了一下关于函数参数和返回值的变体规则,说明了参数逆变与返回值协变的原因。

感谢你的阅读,如果觉得文章不错的话,还希望支持一下博主!

相关文章
|
JavaScript
TypeScript逆变 :条件、推断和泛型的应用
有一个名为 `test` 的函数,它接受两个参数。第一个参数是函数 `fn`,第二个参数 `options` 受到 `fn` 参数的限制。乍一看,这个问题貌似并不复杂,不是吗?糊业务的时候,这种不是常见的需求嘛。
119 1
 TypeScript逆变 :条件、推断和泛型的应用
|
JavaScript Java
带你读《现代TypeScript高级教程》十三、类型兼容:协变和逆变(1)
带你读《现代TypeScript高级教程》十三、类型兼容:协变和逆变(1)
|
JavaScript 安全
带你读《现代TypeScript高级教程》十三、类型兼容:协变和逆变(2)
带你读《现代TypeScript高级教程》十三、类型兼容:协变和逆变(2)
|
JavaScript 安全 Java
《现代Typescript高级教程》协变和逆变
类型兼容:协变和逆变 引言 在类型系统中,协变和逆变是对类型比较(类型兼容)一种形式化描述。在一些类型系统中,例如 Java,这些概念是显式嵌入到语言中的,例如使用extends关键字表示协变,使用super关键字表示逆变。在其他一些类型系统中,例如 TypeScript,协变和逆变的规则是隐式嵌入的,通过类型兼容性检查来实现。
120 0
|
JavaScript 安全
深入 TypeScript 中的子类型、逆变、协变,进阶 Vue3 源码前必须搞懂的。
TypeScript 中有很多地方涉及到子类型 subtype、父类型 supertype 的概念,如果搞不清这些概念,那么很可能被报错搞得无从下手,或者在写一些复杂类型的时候看到别人可以这么写,但是不知道为什么他可以生效。(就是我自己没错了)
|
JavaScript 前端开发
知其然,知其所以然:TypeScript 中的协变与逆变
## 前言 在前一篇文章《淘宝店铺 TypeScript ESLint 规则集考量》中,我们提到了这一条规则:**method-signature-style**,它的作用是对 interface 中不同的函数声明方式进行约束,这里的声明方式主要有两种,_method_ 与 _property_,区别如下: ```typescript // method interface T1 { fu
|
1月前
|
JavaScript 前端开发 安全
深入理解TypeScript:增强JavaScript的类型安全性
【10月更文挑战第8天】深入理解TypeScript:增强JavaScript的类型安全性
45 0
|
1月前
|
JavaScript 前端开发 开发者
深入理解TypeScript:类型系统与实用技巧
【10月更文挑战第8天】深入理解TypeScript:类型系统与实用技巧
|
2月前
|
存储 JavaScript
typeScript进阶(11)_元组类型
本文介绍了TypeScript中的元组(Tuple)类型,它是一种特殊的数组类型,可以存储不同类型的元素。文章通过示例展示了如何声明元组类型以及如何给元组赋值。元组类型在定义时需要指定数组中每一项的类型,且在赋值时必须满足这些类型约束。此外,还探讨了如何给元组类型添加额外的元素,这些元素必须符合元组类型中定义的类型联合。
44 0
|
2月前
|
JavaScript
typeScript进阶(10)_字符串字面量类型
本文介绍了TypeScript中的字符串字面量类型,这种类型用来限制变量只能是某些特定的字符串字面量。通过使用`type`关键字声明,可以确保变量的值限定在预定义的字符串字面量集合中。文章通过示例代码展示了如何声明和使用字符串字面量类型,并说明了它在函数默认参数中的应用。
36 0