类型兼容性(Type Compatibility)

TypeScript中类型兼容性是基于structural subtyping。Structural typing是一种仅根据成员来关联类型的方式,这与nominal typing正好相反。考虑下面代码:

interface Named {
    name: string;
}

class Person {
    name: string;
}

var p: Named;
// OK, because of structural typing
p = new Person();

在C#或Java等nominally-typed语言中,上面的代码会报错,因为Person类没有明确描述自己是Named接口的实现。

TypeScript的structural类型系统的设计是基于JavaScript代码通常是如何编写的。 由于JavaScript广泛使用函数表达式和对象字面量(object literal)等匿名对象,因此利用structural类型系统而不是nominal类型系统可以更自然地表示JavaScript库中存在的各种类型间的关系。



关于健壮性(A Note on Soundness)

TypeScript的类型系统允许在编译时还不知道是否安全的一些操作。当一个类型系统有这种特性时,就称为不是健壮的(“sound“)。TypeScript中允许不健壮行为的地方都经过仔细考虑,本章节中将解释这些地方,以及背后的动机场景。



Starting out

TypeScrip的structural类型系统的基本规则是:如果y中包含x中相同的成员(换另一种说法:y中除了包含x中相同的成员以外,可能还存在其他成员),那么就称x与y兼容(x is compatible with y)。例如:

interface Named {
    name: string;
}

var x: Named;
// y’s inferred type is { name: string; location: string; }
var y = { name: 'Alice', location: 'Seattle' };
x = y;

为了检测y是否可以被赋值给x,编译器检查x的每个属性,发现在y中都有对应的兼容属性。这种情况下,y必须有一个成员‘name’,其类型必须是string。的确有,因此允许赋值。

当检测函数调用参数时,同样的赋值规则适用:

function greet(n: Named) {
    alert('Hello, ' + n.name);
}

greet(y); // OK

注意‘y’还有另外一个‘location’属性,但这不会报错。当检测兼容性时只考虑target类型(代码中是‘Named’ )的成员。

这个比较过程递归处理,检查每一个成员以及子成员的类型。



比较两个函数(Comparing two functions)

虽然比较基本类型(primitive types)与对象类型相对显而易见,但什么样的函数被视为兼容这一问题就有些棘手。先从一个简单例子开始,两个函数只有参数列表不同:

var x = (a: number) => 0;
var y = (b: number, s: string) => 0;

y = x; // OK
x = y; // Error

为了检查x是否可以被赋值给y,我们首先来看参数列表。y中的每个参数都必须有x中对应的参数且类型兼容。注意不考虑参数名称,只考虑参数类型。由于x中每个参数在y中都有对应的类型兼容参数,因此允许赋值。

第二个赋值是错误的,因为y还需要第二个参数但‘x’没有这个参数,所以不允许赋值。

你可能奇怪在这个例子中y = x为什么要允许丢弃(‘discarding’)参数。允许赋值的原因就在于JavaScript中忽略额外的函数参数是非常普遍的。例如Array#forEach 提供了三个参数给回调函数:数组元素、索引、以及包含元素的数组。但提供一个仅使用第一个参数的回调函数是很有用的:

var items = [1, 2, 3];

// Don't force these extra arguments
items.forEach((item, index, array) => console.log(item));

// Should be OK!
items.forEach((item) => console.log(item));

现在来看如何处理返回类型,使用两个只是返回类型不同的函数:

var x = () => ({name: 'Alice'});
var y = () => ({name: 'Alice', location: 'Seattle'});

x = y; // OK
y = x; // Error because x() lacks a location property

类型系统强行要求源函数(source function)的返回类型必须是目标函数(target function)返回类型的子类型。



Function Argument Bivariance

当比较函数参数类型时,如果源参数(source parameter )可赋值给目标参数( target parameter),或者反过来目标参数可赋值给源参数,那么赋值成立。这是不健壮的,因为一个调用方可能给了一个接受更专用类型的函数(a function that takes a more specialized type), 但调用时用一个不专用类型的函数(invokes the function with a less specialized type)。实际中,这种错误非常罕见,允许这一点可以使许多常见的JavaScript patterns成立。看一个简短的例子:

enum EventType { Mouse, Keyboard }

interface Event { timestamp: number; }
interface MouseEvent extends Event { x: number; y: number }
interface KeyEvent extends Event { keyCode: number }

function listenEvent(eventType: EventType, handler: (n: Event) => void) {
    /* ... */
}

// Unsound, but useful and common
// 不健壮,但是有用且常见
listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));

// Undesirable alternatives in presence of soundness
// 考虑到健壮性,不期望的用法
listenEvent(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y));
listenEvent(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y)));

// Still disallowed (clear error). Type safety enforced for wholly incompatible types
// 不允许(存在明显错误)。类型安全检查发现这是完全不兼容的类型。
listenEvent(EventType.Mouse, (e: number) => console.log(e));



可选参数与Rest参数(Optional Arguments and Rest Arguments)

当比较函数兼容性时,可选参数与必选参数是可互换的。源类型额外的可选参数不是一个错误,目标类型的可选参数在目标类型中没有对应的参数不是一个错误。

当函数有一个rest参数时,这个参数被视为它是一个无穷系列的可选参数。

从类型系统视角来看这是不健壮的,但从运行时观点来看,可选参数一般没有well-enforced,因为对大多数函数来说等同于在这个位置上传递‘undefined’。

下面这个例子是函数的常见模式,接受一个回调,但是用某种(对编程人员来说)可预测的但(对类型系统来说)未知数量的参数来激活:

function invokeLater(args: any[], callback: (...args: any[]) => void) {    
    /* ... Invoke callback with 'args' ... */
}

// Unsound - invokeLater "might" provide any number of arguments
// 不健壮- invokeLater可能提供任意数量的参数
invokeLater([1, 2], (x, y) => console.log(x + ', ' + y));

// Confusing (x and y are actually required) and undiscoverable
// 令人疑惑(x与y实际上是必填参数),说不清道不明
invokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y));



函数重载(Functions with overloads)

当函数有重载时,在源类型中的每个重载函数都必须与目标类型中的兼容签名匹配。这确保目标函数可以同源函数一样在各种情况下被调用。有特殊重载签名的函数(Functions with specialized overload signatures),即在重载中使用串字面量(string literals)的函数,当检查兼容性时不使用特殊签名(do not use their specialized signatures when checking for compatibility)。



枚举(Enums)

枚举与number兼容,number与枚举兼容。不同枚举类型的枚举值被视为不兼容。例如:

enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };

var status = Status.Ready;
status = Color.Green;  //error



类(Classes)

类的工作与对象字面量类型和接口相似, 但有一个不同:它们都有一个静态与一个实例类型。当比较一个class类型的两个对象时,只比较实例的成员,静态成员与构造函数不影响兼容性。

class Animal {
    feet: number;
    constructor(name: string, numFeet: number) { }
}

class Size {
    feet: number;
    constructor(numFeet: number) { }
}

var a: Animal;
var s: Size;

a = s;  //OK
s = a;  //OK



类中的私有成员(Private members in classes)

类中的私有成员影响兼容性。当检查一个类实例的兼容性时,如果它包含私有成员,目标类型也必须包含源于同一个类的私有成员。这样就允许一个类赋值给与其超类兼容的变量,当与具有相同shape但有不同继承关系的类是不兼容的。



泛型(Generics)

由于TypeScript是一个structural类型系统,当类型参数用作一个成员的部分类型时,它只影响最终的类型(type parameters only affect the resulting type when consumed as part of the type of a member)。例如:

interface Empty<T> {
}

var x: Empty<number>;
var y: Empty<string>;

x = y;  // okay, y matches structure of x

在上面例子中,x与y兼容,原因是它们的结构并不是以不同方式来使用类型参数。修改前面例子,添加一个成员到Empty<T>中,看看结果如何:

interface NotEmpty<T> {
    data: T;
}

var x: NotEmpty<number>;
var y: NotEmpty<string>;

x = y;  // error, x and y are not compatible

此时,一个带类型参数的泛型类型的行为就如同一个非泛型类型。

对于没有指定类型参数的泛型类型来说,用'any'替换所有未指定的类型参数来检查兼容性。 然后对最终类型(The resulting types)检查兼容性,就像非泛型类型一样。例如:

var identity = function<T>(x: T): T { 
    // ...
}

var reverse = function<U>(y: U): U {    
    // ...
}

identity = reverse;  // Okay because (x: any)=>any matches (y: any)=>any



高级话题(Advanced Topics)



子类型与赋值(Subtype vs Assignment)

上面,我们一直在使用兼容性('compatible'),这在语言规范中并没有术语来定义。在TypeScript中有两种兼容性:子类型与赋值(subtype and assignment)。它们的区别在于:赋值扩展了子类型兼容性,允许to and from 'any'赋值,允许to and from enum用对应的数值赋值。 

语言中不同的地方使用这两种兼容性中的一种,要视情况而定。在实际情况中,类型兼容性是由赋值兼容性所支配,即使在实现与扩展语句等情况下。更多信息可参见 TypeScript spec。




参考资料

[1] http://www.typescriptlang.org/Handbook#type-compatibility

[2] TypeScript系列1-简介及版本新特性, 

[3] TypeScript手册翻译系列1-基础类型, 

[4] TypeScript手册翻译系列2-接口, 

[5] TypeScript手册翻译系列3-类, 

[6] TypeScript手册翻译系列4-模块, 

[7] TypeScript手册翻译系列5-函数, 

[8] TypeScript手册翻译系列6-泛型, 

[9] TypeScript手册翻译系列7-常见错误与mixins, 

[10] TypeScript手册翻译系列8-声明合并, 

[11] TypeScript手册翻译系列9-类型推断,