类型兼容性
TS中的类型兼容性是基于结构子类型的。结构类型是一种只使用其成员来描述类型的方式。 与Java不同,Java的强制转换是基于继承的,只有有继承关系的对象才可以进行转换。
即只要对象中的成员有包含关系,则可以进行类型转换。如,
1 | interface Named { |
这一点也是根据JS的灵活性设计的,JS中变量类型是自动检测的,程序员不需要专门进行规定。
对象的兼容
TS结构化类型系统的基本规则是,如果x
要兼容y
,(所谓x
兼容y
,就是x
可以被赋值为y
),如
1 | interface Named { |
称为x
兼容y
。
这里要检查
y
是否能赋值给x
,编译器检查x
中的每个属性,看是否能在y
中也找到对应属性。 在这个例子中,y
必须包含名字是name
的string
类型成员。y
满足条件,因此赋值正确。
函数的兼容
函数参数兼容
函数的兼容与对象的兼容相反,函数x
的参数包含函数y
的参数,则x
兼容y
。如
1 | let x = (a: number) => 0; |
要查看
x
是否能赋值给y
,首先看它们的参数列表。x
的每个参数必须能在y
里找到对应类型的参数。 注意的是参数的名字相同与否无所谓,只看它们的类型。 这里,x
的每个参数在y
中都能找到对应的参数,所以允许赋值。第二个赋值错误,因为
y
有个必需的第二个参数,但是x
并没有,所以不允许赋值。
这里可以理解为,在函数赋值时,函数的参数可以被忽略的,(注意:函数调用的时候仍然必须赋值相等的参数,否则采用可选参数)但是限制了不能多余参数。即y
中的参数s
,在被赋值给x
的时候直接被忽略,即
1 | //JS中函数调用的灵活性 |
函数返回值兼容
返回参数少的函数可以被赋值为参数多的函数(这里TS称参数多的为参数少的函数的子类),即源函数可以被赋值为子类,即向下转换,如
1 | let x = () => ({name: 'Alice'}); |
类的兼容
类的兼容只比较实例成员。静态部分不会被比较。(仍然是基于成员的比较,而不是Java中的继承),如
1 | class Animal { |
高级类型
交叉类型(Intersection Types)
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如, Person & Serializable & Loggable
同时是 Person
和 Serializable
和 Loggable
。 就是说这个类型的对象同时拥有了这三种类型的成员。
通俗的讲,就是包含所有成员类型的成员属性。如:
1 | function extend<T, U>(first: T, second: U): T & U { //将T与U成员变量混合成为新的变量,并返回该变量,返回时采用交叉类型而不是any |
联合类型(Union Types)
形式:A|B|C
含义:
作为参数时,仅支持A类或B类或C类,其他类型数据均不被接接受。如:
1 | function f(value: number|string){ |
作为返回值时:
1 | interface Bird { |
由于返回的是A|B,所以我们只能访问A与B的交叉成员。
类型保护与区分类型(Type Guards and Differentiating Types)
联合类型可以使我们接受多种特定类型的参数,但是我们要在函数内部更具参数类型执行特定的操作时又该如何判断呢。下面的写法是错误的,因为联合类型在访问任何独有参数时,都会发生错误。
1 | let pet = getSmallPet(); |
为了使代码工作,必须使用类型断言
1 | let pet = getSmallPet(); |
用户自定义的类型保护
注意到,这种写法,我们在任何时候调用参数属性时,都必须加上类型断言。我们可以采用另外一种写法使之后的参数调用不再加上类型断言。
TS中的类型保护机制使之成为现实。 类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。 要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个 类型谓词:
1 | function isFish(pet: Fish | Bird): pet is Fish { |
在这个例子里, pet is Fish
就是类型谓词。 谓词为 parameterName is Type
这种形式, parameterName
必须是来自于当前函数签名里的一个参数名。
每当使用一些变量调用 isFish
时,TypeScript会将变量缩减为那个具体的类型(在以后对应分支中的参数的每次调用都会被识别为该类型),只要这个类型与变量的原始类型是兼容的。
1 | // 'swim' 和 'fly' 调用都没有问题了 |
typeof类型保护
实际上我们可以发现,在上面的断言函数中执行的内容就是判断参数是否为对应类型,是返回true
,否返回false
,所以我们在断言函数内部可以使用typeof
来进行判断。
1 | function isNumber(x: any): x is number { |
更进一步,对于原始类型(number, string, boolean, symbol
),TS直接将typeof
识别为断言函数,而不必我们每次都为原始类型类型编写一个断言。
1 | function padLeft(value: string, padding: string | number) { |
这些* typeof
类型保护*只有两种形式能被识别: typeof v === "typename"
和 typeof v !== "typename"
, "typename"
必须是 "number"
, "string"
, "boolean"
或 "symbol"
。 但是TypeScript并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。
instanceof类型保护
对于非原始类型,如何做到类型判断并收紧呢?TS提供了instanceof
类型保护,也可以避免我们重写断言函数。
1 | interface Padder { |
instanceof
的右侧要求是一个构造函数,TypeScript将细化为:
- 此构造函数的
prototype
属性的类型,如果它的类型不为any
的话 - 构造签名所返回的类型的联合
null和undefined
TS将null,undefined视为两种不同的类型,他们不能被赋值给任何其他类型的参数。如官方文档所说:
注意,按照JavaScript的语义,TypeScript会把 null
和 undefined
区别对待。 string | null
, string | undefined
和 string | undefined | null
是不同的类型。
可选参数与可选属性
使用了 --strictNullChecks
,可选参数会被自动地加上 | undefined
:
1 | function f(x: number, y?: number) { |
也就是说,可选属性可以被赋值为undefined作为占位参数,但是不能被赋值为null。
同样的,可选属性也可被赋值为undefined作为占位参数。
1 | class C { |
类型别名
形如C中的typedef
语法,TS提供了类型别名。
1 | type Name = string; |
起别名不会新建一个类型 - 它创建了一个新 名字来引用那个类型。 给原始类型起别名通常没什么用,尽管可以做为文档的一种形式使用。
同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:
1 | type Container<T> = { value: T }; |
我们也可以使用类型别名来在属性里引用自己:
1 | type Tree<T> = { |
类型别名只能出现在申明的左侧。
字符串字面量类型
字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。
1 | type Easing = "ease-in" | "ease-out" | "ease-in-out"; |
你只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误。
1 | Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"' |
数字字面量类型
TypeScript还具有数字字面量类型。
1 | function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 { |
我们很少直接这样使用,但它们可以用在缩小范围调试bug的时候:
1 | function foo(x: number) { |
换句话说,当 x
与 2
进行比较的时候,它的值必须为 1
,这就意味着上面的比较检查是非法的。
可辨识联合(Discriminated Unions)
你可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做 可辨识联合的高级模式,它也称做 标签联合或 代数数据类型。 可辨识联合在函数式编程很有用处。 一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。 它具有3个要素:
- 具有普通的单例类型属性— 可辨识的特征。
- 一个类型别名包含了那些类型的联合— 联合。
- 此属性上的类型保护。
1 | interface Square { |
首先我们声明了将要联合的接口。 每个接口都有 kind
属性但有不同的字符串字面量类型。 kind
属性称做 可辨识的特征或 标签。 其它的属性则特定于各个接口。 注意,目前各个接口间是没有联系的。 下面我们把它们联合到一起:
1 | type Shape = Square | Rectangle | Circle; |
现在我们使用可辨识联合:
1 | function area(s: Shape) { |
Symbols
这个部分也是ES6的引入一种新的原生类型,如number
,string
。
其特点是唯一,一个symbols是唯一独特的,无法改变的。即使两个symbol
输入的key
一样,这两个值仍然是完全不同的。
symbol
类型的值是通过Symbol
构造函数创建的。
1 | let sym1 = Symbol(); |
Symbols是不可改变且唯一的。
1 | let sym2 = Symbol("key"); |
像字符串一样,symbols也可以被用做对象属性的键。
1 | let sym = Symbol(); |