TypeScript手册翻译系列10-类型兼容性

类型兼容性(Type Compatibility)

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

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。的确有,所以容许赋值。

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

less

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

greet(y); // OK


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

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

ide

比较两个函数(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 提供了三个参数给回调函数:数组元素、索引、以及包含元素的数组。但提供一个仅使用第一个参数的回调函数是颇有用的:

ui

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));


如今来看如何处理返回类型,使用两个只是返回类型不一样的函数:
spa

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)返回类型的子类型。
.net

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-简介及版本新特性, http://my.oschina.net/1pei/blog/493012

[3] TypeScript手册翻译系列1-基础类型, http://my.oschina.net/1pei/blog/493181

[4] TypeScript手册翻译系列2-接口, http://my.oschina.net/1pei/blog/493388

[5] TypeScript手册翻译系列3-类, http://my.oschina.net/1pei/blog/493539

[6] TypeScript手册翻译系列4-模块, http://my.oschina.net/1pei/blog/495948

[7] TypeScript手册翻译系列5-函数, http://my.oschina.net/1pei/blog/501273

[8] TypeScript手册翻译系列6-泛型, http://my.oschina.net/1pei/blog/513483

[9] TypeScript手册翻译系列7-常见错误与mixins, http://my.oschina.net/1pei/blog/513499

[10] TypeScript手册翻译系列8-声明合并, http://my.oschina.net/1pei/blog/513649

[11] TypeScript手册翻译系列9-类型推断, http://my.oschina.net/1pei/blog/513652

相关文章
相关标签/搜索