TypeScript中的类型兼容是基于结构归类的。在普通分类的相比之下,结构归类是一种纯粹用于将其成员的类型进行关联的方法。思考下面的代码:html
interface Named { name: string; } class Person { name: string; } var p: Named; // 正确, 由于这里编译器自动进行结构归类 p = new Person();
如C#、Java这些表面上的类型语言(这里指的“表面上的类型语言”,指C#和Java须要使用“implements”关键字明确指出类实现某个接口才能对应得上其类型),以上的代码便会被看成错误的,由于没有明确指出Person类实现(implements)Named接口。数组
TypeScript的结构类型系统就是基于JavaScript代码典型的写法设计的。由于JavaScript普遍使用匿名对象如函数表达式和字面量对象,使用结构类型系统代替表面上处理将使JavaScript中这些关系体现的更天然。安全
TypeScript类型系统容许执行某些在编译阶段没法肯定安全性的操做。当一个类型系统有这个属性的时候,咱们视之为“不健全的”。TypeScript中容许执行这些操做这一机制是通过仔细考虑的,经过文档咱们将解释什么状况下会发生这种事和容许这些操做后所带来的好的一面。app
跟着代码出发(老司机,带带我...)ide
TypeScript结构类型系统的基本规则如:若是x是兼容y的,那么y至少具备和x相同的属性成员。例如:函数
interface Named { name: string; } var x: Named; // 推断出y的相似是{ name: string; location: string; } var y = { name: 'Alice', location: 'Seattle' }; x = y;
为了检查y是否可以赋值给x,编译器须要检查x的每一个属性,而且在y中找到对应的兼容属性。在这种状况下,y必须有个名为name而且值是字符串的属性。而y知足了这条件,因此可以赋值给x。spa
一样的规则也适用在检查函数调用参数时:设计
// 接着上面的代码 function greet(n: Named) { alert('Hello, ' + n.name); } greet(y); // ok
注意,y有一个额外的"location'属性,但这并未产生错误。只有目标类型的成员(这里的目标类型指“Named”)会被检查是否兼容。code
比较的过程是递归进行的,检查每一个成员及其子成员的类型。htm
两个函数之间的比较
原始类型和对象类型的比较是相对简单的,但问题是被认为是兼容的函数是怎么样的呢。让咱们从一个最基本的例子开始吧,如下两个函数的不一样之处仅仅在于他们的参数列表:
var x = (a: number) => 0; var y = (b: number, s: string) => 0; y = x; // ok x = y; // 错误
若要检查x是否能够赋值给y,首先看参数列表。y中的每一个参数必须在x中都有相应而且类型兼容的参数。主意,参数名可不考虑,只要类型可以对应上。在这个案例中,x的每一个参数在y中都有相应的参数,因此是容许赋值的。第二个赋值是错误的,由于y的第二个属性是必须的,可是x没这个属性,因此不被容许赋值。
你可能对为何在y=x中容许第二个参数而感到疑惑。赋值的过程容许忽略函数额外的参数,这在JavaScript中实际上很常见。例如Array的forEach函数为他的回调函数提供了三个参数:数组元素、索引、包含它的数列。然而,大多用到的也就第一个参数。
var items = [1, 2, 3]; // 这些参数不是强制要求的 items.forEach((item, index, array) => console.log(item)); // 这样也能够 items.forEach((item) => console.log(item));
如今让咱们来看看返回值类型是如何处理的,使用两个仅返回值类型不一样的函数:
var x = () => ({name: 'Alice'}); var y = () => ({name: 'Alice', location: 'Seattle'}); x = y; // ok y = x; // 错误,由于x()缺乏一个属性
类型机制强制要求源函数的返回值类型是目标函数返回值类型的子类型。
可选参数及剩余参数
当对函数的兼容进行比较时,可选和必须的参数是能够互换的。源类型有额外的可选参数不会形成错误,目标类型的可选参数中不存在对应参数也不会产生错误。
当函数有其他的参数,将会被看成无限的可选参数同样来处理。
从类型机制来看这是不健全的,但从代码运行的角度看,可选参数不是强制要求的,由于它至关于在函数参数的对应位置传入一个"undefined"。
下面的例子是个广泛模式的函数,该函数须要传入个回调函数,而且在调用的时候传入可预知(对于开发者而言)可是未知数量(对于类型机制)的参数。
function invokeLater(args: any[], callback: (...args: any[]) => void):void { callback.apply(null,args); } // invokeLater"可能"任何数量的参数 invokeLater([1, 2], (x, y) => console.log(x , y)); invokeLater([3], (x?, y?) => console.log(x , y)); invokeLater([4,5,6,7], (x?, y?) => console.log(x , y));
重载的函数
当一个函数具备重载状况时,源类型的每次重载必须在目标类型上可找到匹配的签名。这确保了目标函数能够在全部源函数可调用的地方调用。当作兼容性检查时,带有特殊签名的函数重载(那些重载时使用字符串)将不会使用他们特殊签名。(详情可见:TypeScript Declaration Merging(声明合并)中的接口合并第二个案例)
枚举
枚举和number相互兼容。不一样枚举类型的枚举值之间是不兼容的。例如:
enum Status { Ready, Waiting }; enum Color { Red, Blue, Green }; var status = Status.Ready; status = Color.Green; // 错误
类
类的兼容和对象字面量类型还有接口的兼容类似,只是有一个不一样:它具备静态类型和实例类型。比较两个类类型的对象时,只比较实例部分的成员。静态部分的成员和构造函数不影响兼容性。
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
类的私有成员
类中的私有成员会影响其兼容性。当一个类的实例进行兼容性检查时,若是它包含一个私有成员,那么目标类型必须也包含一个来源与同一个类的私有成员(详情可参阅:TypeScript Class(类)中的理解Private(私有))。这也形成了一个类能够被赋值为其父类的实例,可是却不能被赋值成另外一个继承其父类的类(虽然他们是同一个类型)。
泛型
由于TypeScript是一个结构类型系统,参数类型只影响将其做为部分红员类型的目标类型(好比有个函数fn(a:string),而后函数中有个变量的某属性值是a,那么a对这个目标类型将产生影响)。
案例:
interface Empty<T> { } var x: Empty<number>; var y: Empty<string>; x = y; // ok, y能够和x的结构相匹配
在上述例子中,x和y是兼容的,由于他们的结构在使用类型参数并没什么不一样之处。改变这个例子,给Empty<T>加个成员,看看会是什么状况:
interface NotEmpty<T> { data: T; } var x: NotEmpty<number>; var y: NotEmpty<string>; x = y; // 错误,x和y不兼容
对于那些没有指定其参数类型的泛型类型,兼容性的检查是经过为全部没指定参数类型的参数使用"any"代替的。而后目标类型检查兼容性,就和非泛型的案例通常。
案例:
var identity = function<T>(x: T): T { // ... } var reverse = function<U>(y: U): U { // ... } identity = reverse; // ok,由于(x: any)=>any和(y: any)=>any能够匹配
深层探讨
子类VS赋值
至此为止,咱们了解了兼容性,它并未在语言规范里定义。在TypeScript中有两种兼容:子类和赋值。它们的不一样点在于,赋值扩展了子类型的兼容性而且可对"any"进行取值和赋值、对枚举进行取对应数值。
根据状况的不一样,TypeScript语言会在不一样的地方使用两种兼容机制中的一种。对于实际运用而言,类型的兼容性是由赋值时的兼容检查或者implements和extends关键字来控制的。更多详情,请参照TypeScript spec (友情提醒,是doc文件下载)