likes
comments
collection
share

TS类型兼容

作者站长头像
站长
· 阅读数 8

介绍

Typescript中的类型是基于类型结构/类型成员来定义的。 这与名义类型有较大区别

结构类型(鸭子理论)

麦克阿瑟将军: 如果他走起来像鸭子,也能像鸭子一样叫,那么他就是一只鸭子

interface Person {
  name: string;
}

interface Student {
  name: string;  
  school: string;
}

declare let p:Person;
declare let s:Student;

// 赋值
p = s; // ok 因为可以在s中找到p中所有需要的属性
s = p; // fail 因为在Person 中找不到 Student属性

名义类型

package domain;

public class HelloWorld {

    public static void main(String[] args) {
        // domain.Student cannot be conveted to domain.Person
        Person s = new Student();
    }

}

public class Person {
    String name;
}

public class Student {
    String name;
    String school;
}

在上面🌰中,我尝试将Student类型的实例赋值给Person,编译器会直接告诉我不能这么做(Student cannot be converted to Person)。

小结

从上面两个栗子,可以得出以下结论,在尝试将一个类型变量赋值给另一个类型变量时(a = b)

  • 结构类型,只关注本身属性是否 被赋值的对象(a)中的属性,是否都能在赋值对象(b)中找到。如果能够赋值,可以进一步的出 b 是 a 的子类型。
  • 名义类型,名义类型的兼容性/等价性是通过明确的声明 / 类型的名称来确定。与结构无关。

开始

基础类型兼容

基础类型我们先拿 number 类型来举例,理解。

let age: number = 18; // ok

age = false  // false
  • 在第一步我们 将age 变量定义为 number 并给他赋值 18 。从这一步我们可以得出
    • 18 是number 的子类型
    • 类型兼容的原则是 具体的 可以赋值给 抽象的。
  • 在第二步我们将 false 赋值给 number。 这一步编辑器报错了。证明 这两个基础类型没有交叉部分。

对象类型兼容

这里可以参照【介绍】中结构类型的部分。 当我们尝试将一个对象,赋值给另一个对象时,需要确保 被赋值的对象中的属性都能在 赋值对象中找到

interface Person {
  name: string;
}

interface Student {
  name: string;  
  school: string;
}

declare let p:Person;
declare let s:Student;

// 赋值
p = s; // ok 因为可以在s中找到p中所有需要的属性
s = p; // fail 因为在Person 中找不到 Student属性

函数兼容

函数类型兼容的case 比较多,我们先从基础类型开始探索。

// 参数一致
type Func1 = (arg1: number) => void;
type Func2 = (arg1: number, arg2: boolean) => void;


declare let func1: Func1;
declare let func2: Func2;

func1 = func2; // 不能将类型“Func2”分配给类型“Func1”。
  						 // Target signature provides too few arguments. Expected 2 or more, but got 1.

func2 = func1; // ok 省略了第二个参数。就像我们日常使用forEach/map 传入的函数也会省略第二或第三个参数

// 返回值一致
type Func3 = () => { name: string }

type Func4 = () => { name: string; age: number };

declare let func3: Func3;
declare let func4: Func4;

func3 = func4; 
func4 = func3; // 不能将类型“Func3”分配给类型“Func4”。
  						 // 类型 "{ name: string; }" 中缺少属性 "age",但类型 "{ name: string; age: number; }" 中需要该属性。

从上面可以推断出 如果两个函数需要兼容 a=b; 那么需要

  • 参数: b函数中的参数必须都能在a 函数的参数中找到。
  • 返回值: b 函数返回值必须是 a函数返回值的子类型。

有同学可能已经从上面的结论中看出了一个比较奇怪的现象。 如果要使a=b成立,那么a 的参数类型需要时 b参数类型的子类型!

从上面的结论中我们需要先引进【协变】与 【逆变 】两个概念。

协变与逆变

引用维基百科的定义

在一门程序设计语言的类型系统中,一个类型规则或者类型构造器是:

  • 协变(covariant),如果它保持了子类型序关系≦。该序关系是:子类型≦基类型。
  • 逆变(contravariant),如果它逆转了子类型序关系。

考虑数组类型构造器: 从Animal类型,可以得到Animal[](“animal数组”)。 是否可以把它当作

  • 协变:一个Cat[]也是一个Animal[]
  • 逆变:一个Animal[]也是一个Cat[]

还是函数赋值的例子,我们这次构造一个Animal, Dog,Corgi的例子。

目标函数接受一个Dog -> Dog的类型

interface Animal {};

interface Dog extends Animal {
    bark: () => {}
}

interface Corgi extends Dog {
    smile: () => {}
}

declare let Dog2Dog: (d: Dog) => Dog;

declare let Animal2Corgi: (d: Animal) => Corgi;
declare let Corgi2Animal : (d: Corgi) => Animal;

Dog2Dog = Animal2Corgi; // ok
Dog2Dog = Corgi2Animal; // 不能将类型“(d: Corgi) => Animal”分配给类型“(d: Dog) => Dog”。参数“d”和“d” 的类型不兼容。
   											// 类型 "Dog" 中缺少属性 "smile",但类型 "Corgi" 中需要该属性。

从上面可以看出的是,只有Animal2Corgi 可以赋值给 Dog2Dog。

我们分别从参数 以及 返回值来分析。

  • 参数 Animal 是 Dog 的父类型, 函数参数是逆变。
  • 返回值 Corgi 是 Dog的子类型, 函数返回值是协变。

总结

在从全文回归,我们知道 ts使用的是结构类型,一个对象的类型,只有本身的属性决定。并且后来我们在对比函数兼容时,了解到函数参数(逆变)与函数返回值(协变)的兼容区别。

转载自:https://juejin.cn/post/7310412252553117705
评论
请登录