一.类型推断
赋值推断
类型推断机制减轻了强类型带来的语法负担,例如:
let x = 3;
// 等价于
let x: number = 3;
编译器能够根据变量初始值3
推断出变量类型是number
,因此多数场景下不必显式声明类型,它猜得到
P.S.即使在一切都要提前确定类型的Haskell中,也并非处处充斥着类型声明,而是相当简洁,正是因为编译器提供了强大的类型推断支持
在类似赋值的场景能够根据目标值来确定类型,具体如下:
变量或(类)成员初始值
参数默认值
函数返回值
这3类值都能提供直接的类型信息,进而确定目标类型。除此之外,还有一些不那么直接的场景,比如数组类型
数组类型
let x = [0, 1, null];
数组中的元素除了number
就是null
,而number
“兼容”null
,因此推断x
的类型是number[]
Null、Undefined和Never是其它类型的子类型,因此可以赋值给任何其它类型变量
也就是说,要确定数组类型的话,先要确定每个元素的类型,再考虑其兼容关系,最终确定一个最“宽”的类型(包容数组中所有其它类型,称为best common type)作为数组类型
如果数组元素中没有一个能够兼容其它所有类型的类型(即找不出best common type),就用联合类型,例如:
// 推断 mixin: (string | number | boolean)[]
let mixin = [1, '2', true];
class Animal {}
class Elephant extends Animal {}
class Snake extends Animal {}
// 推断 zoo: (Elephant | Snake)[]
let zoo: Animal[] = [new Elephant(), new Snake()];
上下文推断
与赋值推断相比,上下文推断是另一种不同的思路:
推断
值 ------> 变量类型
查找 匹配(推断)
上下文 -----> 上下文类型 -----------> 变量类型
前者从值到类型,后者从类型到类型。根据上下文得出类型信息,再按位置映射到变量上,例如:
// 推断 mouseEvent: MouseEvent
window.onmousedown = function(mouseEvent) {
// ...
};
右侧匿名函数作为mousedown
事件处理器,遵从DOM API的类型约束,从而得出参数类型:
interface MouseEvent extends UIEvent {
readonly clientX: number;
readonly clientY: number;
//...等等很多属性
}
interface GlobalEventHandlers {
onmousedown: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
}
interface Window extends GlobalEventHandlers {/*...*/}
declare var window: Window;
(摘自TypeScript/lib/lib.dom.d.ts)
如果脱离了mousedown
事件处理器这个上下文,就推断不出参数类型了:
// 推断 mouseEvent: any
function handler(mouseEvent) {
console.log(mouseEvent.clickTime);
}
很多场景都会根据上下文推断类型,例如:
函数调用中的参数
赋值语句的右侧
类型断言
对象成员和数组字面量
return
语句
二.子类型兼容性
TypeScript的13种基本类型中,类型层级关系如下:
简单总结:
Any最“宽”。兼容其它所有类型(换言之,其它类型都是Any的子类型)
Never最“窄”。不兼容任何其它类型
Void兼容Undefined和Null
其它类型都兼容Never和Void
P.S.兼容可以简单理解可否赋值(文末有严谨描述),例如:
let x: any;
let y: number;
let z: null;
// Any兼容Number
x = y;
// Number兼容Null
y = z;
// Null不兼容Number
// 错误 Type 'number' is not assignable to type 'null'.
z = y;
不只基本类型有层级,函数、类、泛型等复杂类型间也有这样的兼容关系
三.函数
兼容性判定
对类型系统而言,需要准确判断一个函数类型是否兼容另一个函数类型,例如在赋值的场景:
let log: (msg: string) => void
let writeToFile: (msg: any, encode: string) => void
// 类型兼容吗?该不该报错
log = writeToFile;
writeToFile = log;
从类型安全角度来看,把log
换成writeToFile
不安全(缺encode
参数,writeToFile
不一定能正常工作),反过来的话是安全的,因为返回值类型相同,参数绰绰有余,msg
的类型也兼容(string
是any
的子类型)
具体的,TypeScript类型系统对函数类型的兼容性判定规则如下:
参数:要求对应参数的类型兼容,数量允许多余
let x = (a: number) => 0; let y = (b: number, s: string) => 0; y = x; // OK x = y; // Error
返回值:要求返回值类型兼容
let x = () => ({name: "Alice"}); let y = () => ({name: "Alice", location: "Seattle"}); x = y; // OK y = x; // Error, because x() lacks a location property
函数类型:要求满足双变性约束
函数类型的双变性(bivariance)
双变是指同时满足协变和逆变,简单地讲:
协变(covariant):允许出现父类型的地方,也允许出现子类型,即里氏替换原则
逆变(contravariant):协变反过来,即允许出现子类型的地方,也允许出现父类型
双变(bivariant):同时满足协变和逆变
不变(invariant或nonvariant):既不满足协变也不满足逆变
协变很容易理解,子类型兼容父类型,此外还具有一些(父类型不具备的)扩展属性或方法,因此用子类型换掉父类型后,仍能正常工作(类型安全)
而逆变并不很直观,什么场景下,用父类型换掉子类型后,仍能保证类型安全呢?
继承关系中的成员函数重写,算是逆变的典型例子:
class Example {
foo(maybe: number | undefined) { }
str(str: string) { }
compare(ex: Example) { }
}
class Override extends Example {
foo(maybe: number) { } // Bad: should have error.
str(str: 'override') { } // Bad: should have error.
compare(ex: Override) { } // Bad: should have error.
}
(摘自Overridden method parameters are not checked for parameter contravariance)
对比重写前后的函数类型:
// foo
(maybe: number | undefined) => any
(maybe: number) => any
// str
(str: string) => any
(str: 'override') => any
// compare
(ex: Example) => any
(ex: Override) => any
P.S.str(str: 'override')
比str(str: string)
“窄”个undefined
,默认值使得参数值集少了undefined
参数都从“宽”的类型变成了更“窄”的类型,即从父类型变为子类型,显然,这样做是不安全的,例如:
function callFoo(example: Example) {
return example.foo(undefined);
}
callFoo(new Example()); // 没问题
callFoo(new Override()); // 可能会出错,因为子类的foo不接受undefined
相反地,如果子类重写后的参数类型更“宽”,那么就是安全的,例如:
class Example {
foo(maybe: number | undefined) { }
}
class Override extends Example {
foo(maybe: number) { } // Sound
}
这就是所谓的逆变,对成员函数的参数而言,用父类型换掉子类型是安全的,即:
允许出现子类型的地方,也允许出现父类型
从类型角度来看,子类型允许类型之间有层级(继承)关系,从宽泛类型到特殊类型,而协变、逆变等关系就建立在这种类型层级之上:
协变:简单类型的层级关系保留到了复杂类型,这个复杂类型就是协变的,例如
Animal
是Cat
的父类型,而数组Animal[]
也是Cat[]
的父类型,所以数组类型是协变的逆变:简单类型的层级关系到复杂类型中反过来了,这个复杂类型就是逆变的。例如函数类型
Animal => string
是Cat => string
的子类型(因为后者接受的参数更“窄”),而简单类型Animal
是Cat
的父类型,那么函数类型就是逆变的
P.S.如我们所见,逆变并不直观,因此为了保持类型系统简单,有的语言也认为类型构造器不变,虽然这样可能会违反类型安全
特殊地,TypeScript里的函数类型是双变的,例如:
interface Comparer<T> {
compare(a: T, b: T): number;
}
declare let animalComparer: Comparer<Animal>;
declare let dogComparer: Comparer<Dog>;
animalComparer = dogComparer; // Ok because of bivariance
dogComparer = animalComparer; // Ok
理论上应该要求函数参数逆变,以确保类型安全,因此:
// 把父类型赋值给子类型,在逆变的场景中是安全的
dogComparer = animalComparer; // Ok
// 把子类型赋值给父类型,在逆变的场景(函数类型)中是不安全的
animalComparer = dogComparer; // Ok because of bivariance
后者不安全,但在JavaScript世界里很常见:
This is unsound because a caller might end up being given a function that takes a more specialized type, but invokes the function with a less specialized type. In practice, this sort of error is rare, and allowing this enables many common JavaScript patterns.
所以TypeScript并没有强制约束函数类型逆变,而是允许双变。更进一步地,在比较两个函数类型时,只要一方参数兼容另一方的参数即可,如上例中dogComparer
与animalComparer
能够相互赋值
可选参数和剩余参数
比较参数兼容性时,不要求匹配可选参数,比如原类型具有额外的可选参数是合法的,目标类型缺少相应的可选参数也是合法的
对于剩余参数,就当成是无限多个可选参数,也不要求严格匹配。虽然从类型系统的角度来看不安全,但在实际应用中是一种相当常见的“模式”,例如用不确定的参数调用回调函数:
function invokeLater(args: any[], callback: (...args: any[]) => void) {
/* ... Invoke callback with 'args' ... */
}
// Unsound - invokeLater "might" provide any number of arguments
invokeLater([1, 2], (x, y) => console.log(x + ", " + y));
函数重载
对于存在多个重载的函数,要求源函数的每个重载版本在目标函数上都有对应的版本,以保证目标函数可以在所有源函数可调用的地方调用,例如:
interface sum {
(a: number, b: number): number;
(a: number[]): number;
}
// Sum要求的两个版本
function add(a: any, b: any);
function add(a: any[], b?: any): any;
// 额外的版本
function add(a: any[], b: any, c: number): any;
function add(a, b) { return a; }
let sum: sum = add;
sum
函数有两个重载版本,所以目标函数至少要兼容这两个版本
四.枚举
首先,来自不同枚举类型的枚举值不兼容,例如:
enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };
let s = Status.Ready;
// Type 'Color.Green' is not assignable to type 'Status'.
s = Color.Green; // Error
特殊地,数值枚举与数值相互类型兼容,例如:
enum Status { Ready, Waiting };
// 数值兼容枚举值
let ready: number = Status.Ready;
// 枚举值兼容数值
let waiting: Status = 1;
但字符串枚举并不与字符串类型相互兼容
enum Status { Ready = '1', Waiting = '0' };
let ready: string = Status.Ready;
// 报错 Type '"0"' is not assignable to type 'Status'.
let waiting: Status = '0';
P.S.虽然从实际类型上看,上例赋值是合法的,但在类型系统中认为二者不兼容,因此报错
五.类
类与对象字面量类型和接口类似,区别在于,类同时具有实例类型和静态类型,而比较两个类实例时,仅比较实例成员
因此,静态成员和构造函数并不影响兼容性:
class Animal {
static id: string = 'Kitty';
feet: number;
constructor(name: string, numFeet: number) { }
}
class Size {
feet: number;
constructor(numFeet: number) { }
}
let a: Animal;
let s: Size;
a = s; // OK
s = a; // OK
私有成员和受保护成员
成员修饰符private
和protected
也会影响类型兼容性,具体地,要求这些成员源自同一个类,以此保证父类兼容子类:
class Animal {
private feet: number;
constructor() { }
}
class Cat extends Animal { }
// 和Animal长得完全一样的Tree
class Tree {
private feet: number;
constructor() { }
}
// 正确 父类兼容子类
let animal: Animal = new Cat();
// 错误 Type 'Tree' is not assignable to type 'Animal'.
animal = new Tree();
// 正确 二者“形状”完全一样
let kitty: Cat = new Animal();
把Tree
实例赋值给Animal
类型会报错,虽然二者看起来完全一样,但私有属性feet
源自不同的类:
Types have separate declarations of a private property ‘feet’.
同样的,上例中把Animal
实例赋值给Cat
类型之所以不报错,是因为二者成员列表相同,并且私有属性feet
也源自同一个Animal
类
六.泛型
interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;
x = y; // OK, because y matches structure of x
y = x; // OK, because x matches structure of y
尽管Empty<number>
与Empty<string>
差别很大,但泛型定义中并没有用到类型参数T
(类似于unused variable,没什么意义),因此互相兼容
interface NotEmpty<T> {
data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;
// 错误 Type 'Empty<string>' is not assignable to type 'Empty<number>'.
x = y;
此时,指定了类型参数的泛型与一般具体类型一样严格比较,对于未指定类型参数的泛型,就当类型参数是any
,再进行比较,例如:
let identity = function<T>(x: T): T {
//...
return x;
}
let reverse = function<U>(y: U): U {
//...
return y;
}
// 正确 等价于把(y: any) => any赋值给(x: any) => any
identity = reverse;
七.类型兼容性
实际上,TypeScript规范中只定义了2种兼容性,子类型兼容性与赋值兼容性,二者存在细微的区别:
Assignment extends subtype compatibility with rules to allow assignment to and from any, and to and from enum with corresponding numeric values.
赋值兼容性扩展了子类型兼容性,允许any
相互赋值,以及enum
和对应数值相互赋值
至于类型兼容性,规范中并未定义这个概念,在多数语境下,所谓的类型兼容性遵从赋值兼容性,implements
和extends
子句也不例外
class Example { foo(maybe: number | undefined) { } }
class Override extends Example { foo(maybe: number) { } // Sound }
这里是不是写错了,明明是变窄,你说成变宽