TypeScript 给 JavaScript 添加了一套静态类型系统,是为了保证类型安全的,也就是保证变量只能赋同类型的值,对象只能访问它有的属性、方法。
比如 number 类型的值不能赋值给 boolean 类型的变量,Date 类型的对象就不能调用 exec 方法。 这是类型检查做的事情,遇到类型安全问题会在编译时报错。
但是这种类型安全的限制也不能太死板,有的时候需要一些变通,比如子类型是可以赋值给父类型的变量的,可以完全当成父类型来使用,也就是“型变(variant)”(类型改变)。 这种“型变”分为两种,一种是子类型可以赋值给父类型,叫做协变(covariant),一种是父类型可以赋值给子类型,叫做逆变(contravariant)。
子类型(subtype)
子类型的作用:可以定义不依赖于细节的行为。换句话说,可以创建接受父类型作为参数的函数,然后您可以使用子类型调用该函数。
function getName(person: Person) : string {
return person.name;
}
// 父类型
const person: Person = {
name: 'lily',
age: 0,
}
// 子类型
const student: Student = {
name: 'xiaoming',
age: 8,
stuNo: 1234,
}
getName(person); // 'lily';
getName(student); // 'xiaoming';
// 定义的时候用父类, 调用的时候可以传子类
很明显,子类型让代码的复用性变得更高,不必为父类型和子类型各自写一个 getName 方法。
可赋值性(assignable)
assignable 是类型系统中很重要的一个概念,当你把一个变量赋值给另一个变量时,就要检查这两个变量的类型之间是否可以相互赋值。
// 父类型
interface Person {
name: string;
age: number;
}
// 子类型
interface Student {
name: string;
age: number;
stuNo: number;
}
let person: Person = {
name: 'lei',
age: 25
}
let student: Student = {
name: 'xiao ming',
age: 8,
stuNo: 11024
}
例如上面声明的两个类型,父类型 Person 和子类型 Student。
// 可赋值性比较
person = student; // ok
student = person; // error
首先,为什么 Person 是父类,明明它的属性更少?在集合论中,属性多的才是父集,属性少的是子集;但是在类型编程中,属性更少的是父类,属性多的是子类。在类型体操中经常会看到 T extends {}这种代码,其实就是因为 {} 是父类型,而 T 是 子类型。{}就类似于顶级类型了。
然后,为什么 person = student 是可以的?而 student = person 又不可以呢?下面以一个函数的声明和调用为例子:
// 这里定义了一个 log 函数,
// 函数参数是 Person 类型
function log(p: Person): void {
console.log(p.name);
console.log(p.age);
}
// 此时,传入一个 Student 类型可以吗?
log(student);
// 打印 'xiaoming'
// 打印 8
可以看到,定义一个函数的时候,正常情况下参数里面的所有属性和值都会用得上【比如这里的 name 和 age】,如果传进来的参数类型包含定义这个函数时的参数类型,那么这个函数一定是可以正常运行的,但是如果情况相反,定义函数时,用的是属性比较多的类型【子类型】,而调用时传入的是属性比较少的类型【父类型】,那么可能会导致,函数内部想要调用某个子类型的属性而调用不到的问题,如下所示:
// 这里定义了一个 log 函数,
// 函数参数是 Student 类型
function log(s: Student): void {
console.log(s.name);
console.log(s.age);
console.log(s.stuNo); // 定义时不报错
}
// 此时,传入一个 Person 类型可以吗?
log(person);
// 打印 'xiaoming'
// 打印 8
// 运行时报错
为什么 person = student 这样赋值是可以的呢?因为此时 Student 类型的属性是完全包含 Person 类型的属性的。这样赋值后,person可以调用他的 name, age 属性并且不会产生类型安全问题。 【但是调用 stuNo 这个属性的话,ts会报错,然而转换为js后,这个person对象上面确实会存在 stuNo 这个属性】。
辅助类型
为了更好的区分两个类型是否是父子类型,这里提供一个 IsSubtypeOf 辅助类型:
type IsSubtypeOf<SubType, ParentType> = SubType extends ParentType ? true : false;
IsSubTypeOf 用于判断一个类型是否是另外一个类型的子类型。
type T11 = IsSubtypeOf<Student, Person>;
// type T11 = true
type T12 = IsSubtypeOf<'hello', string>;
// type T12 = true
type T13 = IsSubtypeOf<42, number>;
// type T13 = true
type T14 = IsSubtypeOf<Map<string, string>, Object>;
// type T14 = true
协变(covariant)
协变的定义是:一个类型T是协变的,如果有S <: P,则T<S> <: T<P>。这个定义比较难理解。还是以上面的 Person 类型 和 Student 类型为例。因为 Student 类型是 Person 类型的子类,记作: Studuent <: Person。请问 Promise<Student> 是否仍然是 Promise<Person> 的子类型?即:是否Promise<Student> <: Promise<Person>?如果满足这个条件,那么就可以说 Promise类型是协变的。
type T21 = IsSubtypeOf<Promise<Student>, Promise<Person>>
// type T21 = true
// 说明 Promise<Student> 是 Promise<Person> 的子类型
由此:有 Student <: Person,则Promise<Student> <: Promise<Person>成立。这表明Promise类型是协变的。
再比如,我们写一个类型MakeArray。
type MakeArray<T> = T[];
type Persons = MakeArray<Person>;
type Students = MakeArray<Student>;
type T31 = IsSubtypeOf<Students, Persons>;
// T31 = true
// 说明类型构造器 MakeArray<T> 是协变的。
逆变(contravariant)
逆变的定义是: 一个类型T是逆变的,如果有S <: P,则 T<P> <: T<S>。简而言之,逆变就是调换了父子类型的顺序。
以下面的类型为例:
type Func<Param> = (param: Param) => void;
Func<Param>创建了具有一个类型参数的函数类型。
请问:如果 Student <: Person,那么,下面两个表达式那个为真?
Func<Person> <: Func<Student>Func<Student> <: Func<Person>
用上面的 IsSubtypeOf 辅助类型试试:
type T41 = IsSubtypeOf<Func<Person>, Func<Student>>
// T41 = true
type T42 = IsSubtypeOf<Func<Student>, Func<Person>>
// T42 = false
我们发现,T41 才是成立的,这表明:Func<Person> 是 Func<Student> 的子类型,和原来的 Student 是 Person 的子类型正好相反。
这说明 Func 类型的行为是逆变的。一般来说,函数类型在参数类型方面是逆变的。
接下来看一段代码:
type Func = (a: string) => void;
const func: Func = (a: 'hello') => undefined
这段代码会报错吗?答案是:会。并且在函数参数的位置是会报错的,而返回值的位置并不会报错。原因是 返回值的位置是协变的,而 undefined 是 void 的子类型,所以并不会报错,同时函数参数的位置是逆变的,而 'hello' 类型是 string类型的子类型而不是父类型,所以这里会报错。
函数类型的参数类型是逆变的,而返回类型协变的。
双向协变
在Typescript 2.6 版本之前,是支持双向协变的,即:既可以把父类型赋值给子类型,又可以把子类型赋值给父类型。这样很明显是有问题的【可能调用到不存在的属性或者方法】,可以把 tsconfig.json 中设置 strictFunctionTypes 为 true 来开启严格检查,这样Typescript就不支持双向协变了。
true:只支持逆变
false:支持双向协变
不变(invariant)
非父子类型之间不会发生型变,只要类型不一样就会报错。