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