TS类型兼容

介绍

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

结构类型(鸭子理论)

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

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属性

名义类型

typescript 复制代码
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 类型来举例,理解。

ini 复制代码
let age: number = 18; // ok

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

对象类型兼容

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

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属性

函数兼容

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

typescript 复制代码
// 参数一致
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的类型

typescript 复制代码
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使用的是结构类型,一个对象的类型,只有本身的属性决定。并且后来我们在对比函数兼容时,了解到函数参数(逆变)与函数返回值(协变)的兼容区别。

相关推荐
kyriewen5 小时前
Anthropic 估值逼近万亿美元,Claude Sonnet 5 + Claude Science 一天两连发
前端·ai编程·claude
小徐_23337 小时前
Wot UI 2.2.0 发布:Button 新增 subtle,VideoPreview 预览体验继续增强
前端·微信小程序·uni-app
天蓝色的鱼鱼9 小时前
关于 CSS 你可能不知道的属性,但关键时刻很有用
前端·css
泯泷10 小时前
第 2 篇:设计第一套字节码:Opcode、Instruction 与 Constant Pool
前端·javascript·安全
妙码生花10 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(十五):优化细节、网络请求封装
前端·后端·ai编程
泯泷10 小时前
第 1 篇:从 1 + 2 开始:亲手写出第一台 JSVM
前端·javascript·安全
团团崽_七分甜10 小时前
Spring Boot 核心知识点总结
前端
lichenyang45310 小时前
从一个按钮开始,理解 ASCF 框架到底在做什么
前端
古夕10 小时前
第三方 SSO 接入实践:redirect_uri 编码、回调一致性与跨项目联调
前端·vue.js