交叉类型( Intersection Types
)是将多个类型合并成一个类型,使用 &
符号连接多个类型,包含所有类型的特性。
function extend<T, U>(first: T, second: U): T & U {
let ret = {} as any
for (let key in first) {
ret[key] = first[key]
}
for (let key in second) {
if (!ret.hasOwnProperty(key)) {
ret[key] = second[key]
}
}
return <T & U>ret
}
class Square {
name: string
length: number
constructor(length: number) {
this.name = 'square'
this.length = length
}
}
class Circle {
name: string
pi: number
radius: number
constructor(radius: number) {
this.name = 'circle'
this.pi = 3.14
this.radius = radius
}
}
let squ = new Square(1)
let cir = new Circle(2)
let rectangle = extend(squ, cir)
console.log(rectangle) //{ name: 'square', length: 1, pi: 3.14, radius: 2 }
联合类型(Union Types
)表示一个值可以是几种类型之一,使用 |
符号连接多个类型。
function padLeft(str: string, padding: string | number) {
if (typeof padding === 'string') {
return padding + str
} else {
return new Array(padding + 1).join(' ') + str
}
}
console.log(padLeft('abc', 4))
console.log(padLeft('abc', 'cba'))
联合类型允许值为多种类型之一,但是该值只能调用多种类型所共有的成员,因为没有确定该值究竟是哪种类型。
interface Bird {
eat(): void
fly(): void
}
interface Fish {
eat(): void
swim(): void
}
function getRandomPet(): Bird | Fish {
let eat = function(): void {
console.log('eat')
}
let fly = function(): void {
console.log('fly')
}
let swim = function(): void {
console.log('swim')
}
let flag = Math.random() > 0.5
if (flag) {
let bird: Bird = { eat, fly }
return bird
} else {
let fish: Fish = { eat, swim }
return fish
}
}
let pet: Bird | Fish = getRandomPet()
pet.eat() // ok
pet.swim() // 错误,swim方法不是公共的成员
如上面代码所示,pet
只能调用 eat()
方法,如果需要确定 pet
为哪种具体类型,从而调用该类型特有的方法,需要使用 类型断言
:
if((pet as Fish).swim) {
;(pet as Fish).swim()
}else {
;(pet as Bird).fly()
}
注意到为了确定 pet
的具体类型,调用该类型特有的方法,需要多次使用 类型断言
,非常麻烦。
TypeScript
提供了 类型保护
机制来避免多次使用 类型断言
,一旦检查过变量的类型之后,可以确定某个作用域中该变量的类型。
要定义一个 类型保护
,需要定义一个函数,它的返回值为一个 类型谓词
:
function isFish(pet: Bird | Fish): pet is Fish {
return (pet as Fish).swim !== undefined
}
这个例子中 pet is Fish
就是 类型谓词
。 谓词的形式为: parameterName is Type
,其中 parameterName
是当前函数的一个参数。
每当调用 isFish()
之后,TypeScript
会将传入的变量缩减为具体的类型:
if(isFish(pet)){
pet.swim()
}else {
pet.fly() // TypeScript还能确定else分支中是Bird类型
}
对于原始类型 number
, string
, boolean
, symbol
,不需要写一个函数来实现类型保护,可以直接使用 typeof
来实现 类型保护
。
注意:
对于undefined
和null
,直接使用arg === undefined
,arg === null
即可实现类型保护;
typeof null
的值为object
。
function padLeft(str: string, padding: string | number) {
if (typeof padding === 'string') {
return padding + str
}
if( typeof padding === 'number'){
return new Array(padding + 1).join(' ') + str
}
throw new Error(`Expected string or number, got '${padding}'.`)
}
instanceof
类型保护类似 typeof
,它是通过构造函数来实现细化类型的。
class Dog {
name: string
bark: () => void
constructor() {
this.name = 'dog'
this.bark = function() {
console.log('wang wang')
}
}
}
class Cat {
name: string
climb: () => void
constructor() {
this.name = 'cat'
this.climb = function() {
console.log('climb')
}
}
}
function getPet(): Dog | Cat {
if (Math.random() < 0.5) {
return new Dog()
} else {
return new Cat()
}
}
let pet: Dog | Cat = getPet()
if (pet instanceof Dog) {
pet.bark()
} else {
pet.climb()
}
开启 --strictNullChecks
选项后:
null
或 undefined
, 需要使用 联合类型
明确地包含它们:
let s: string = '12'
s = undefined // error
s = null // error
let sn: string | null | undefined = '12'
sn = undefined // ok
sn = null // ok
| undefined
:
function f(x: number, y?: number) {
return x + (y || 0)
}
f(1, 2)
f(1, undefined)
interface Optional {
name?: string
}
let o: Optional = {
name: 'o'
}
o.name = undefined
由于可以为 null
或 undefined
类型是通过 联合类型
来实现的,所以需要使用 类型保护
来去除 null
或 undefined
。 null
和 undefined
的类型保护的写法与 JavaScript
中一致:
function checkType(arg: string | null | undefined) {
if (arg === null) {
console.log('null')
} else if (arg === undefined) {
console.log('undefined')
} else {
console.log('string')
}
}
checkType('123')
checkType(null)
checkType(undefined)
也可以使用短路运算符去除 null
和 undefined
:
function f(s: string | undefined) {
return s || 'default'
}
如果编译器无法自动去除 null
和 undefined
,可以通过 类型断言
手动去除,语法是添加 !
后缀,arg!
表示从 arg
的类型中去除 null
和 undefined
:
下面的例子中使用了嵌套函数,编译器无法去除嵌套函数中的 null
(除非是立即调用函数),此时就需要使用 null和undefined的类型断言语法
来手动去除 null
或 undefined
:
function getInitial(name?: string | null) {
name = name || 'Bob'
function upperCase() {
// return name.toUpperCase() // 错误,编译器无法去除 null 和 undefined
return name!.toUpperCase()
}
return upperCase().charAt(0)
}
接口
无法描述 交叉类型
, 联合类型
, 元组
等类型,这些时候通常需要使用到 类型别名
。
类型别名
用于给一个类型起一个别名,该类型通常为 交叉类型
,联合类型
, 对象字面量类型
, 元组
等(也可以作用于原始类型,但是通常没什么用)。
注意:
类型别名
不会新建一个类型,而是创建一个名字引用那个类型。
interface Person{}
interface Serializable{}
type SerializablePerson = Person & Serializable // 给交叉类型起别名
type PersonOrSerializable = Person | Serializable // 给联合类型起别名
type Alias = { // 给对象字面量起别名
name: string
}
type BookInfo = [string, string, number] // 给元组起别名
type MyString = string // 给原始类型string起别名,通常没什么意义
一个特殊的例子, 给 索引类型查询操作符(keyof)
得到的类型起别名:
interface Person {
name: string
age: number
}
type KeyOfPerson = keyof Person
let k: KeyOfPerson
k = 'name' // ok
k = 'age' // ok
k = 'abc' // 错误,只能为Person属性名中的一个
和接口一样,类型别名
也可以是泛型:
// 泛型形式的 类型别名
type Container<T> = { value: T }
// 类型别名 在属性中引用自己
type Node<T> = {
value: T
left: Node<T>
right: Node<T>
}
// 类型别名 与 交叉类型 一起使用
type LinkedList<T> = T & { next: LinkedList<T>}
交叉类型
, 联合类型
, 元组
等, 通常会使用 类型别名
;类型别名
不会创建新的类型,只是创建一个新的名字引用那个类型,而 接口
创建新的类型;类型别名
无法被 extends
和 implements
(同时也不能 extends
其他类型),而 接口
可以。字符串字面量类型
用来指定字符串必须的固定值。实际应用中, 字符串字面量类型
可以与 联合类型
, 类型保护
, 类型别名
配合使用,实现类似枚举类型的字符串:
type ResponseStatus = 'Ok' | 'NotFound' | 'Forbidden'
function getStatusCode(status: ResponseStatus): number {
switch (status) {
case 'Ok':
return 200
case 'NotFound':
return 404
case 'Forbidden':
return 401
default:
throw new Error('unknown response status')
}
}
console.log(getStatusCode('Forbidden')) // 401
console.log(getStatusCode('BadRequest')) // 错误,只能传递三种允许的字符串之一
可以结合 单例类型
, 联合类型
, 类型保护
, 类型别名
来创建一个叫做 可辨识联合
的高级模式。
可辨识联合
具有以下3个要素:
可辨识联合
例子如下:
interface Bird {
kind: 'bird'
tweet(): void
}
interface Cat {
kind: 'cat'
mew(): void
}
interface Duck {
kind: 'duck'
quack(): void
}
首先定义三个接口,每个接口都有 kind
属性但有不同的字符串字面量类型。 kind
属性称作 可辨识的特征
,接下来使用 类型别名
将它们联合起来形成 可辨识联合
:
type Animal = Duck | Bird | Cat
然后就可以使用 可辨识联合
:
function getSound(animal: Animal): void {
switch (animal.kind) {
case 'bird':
return animal.tweet()
case 'cat':
return animal.mew()
case 'duck':
return animal.quack()
default:
throw new Error('unknown animal')
}
}
this类型
用于函数的返回值,能够保证接口的连贯性,实现链式调用。
class Calculator {
protected value: number
constructor(value: number) {
this.value = value
}
printValue(): void {
console.log(this.value)
}
add(num: number): this {
this.value += num
return this
}
multiply(num: number): this {
this.value *= num
return this
}
}
new Calculator(1)
.add(2)
.multiply(3)
.printValue()
继承自该类的子类可以继续使用这些方法,无需修改:
class ScientificCalculator extends Calculator {
sin(): this {
this.value = Math.sin(this.value)
return this
}
}
new ScientificCalculator(0)
.sin()
.add(2)
.printValue()
使用 索引签名
,编译器能够检查使用了动态属性名的代码。一个常见的例子是从对象中选取属性的子集:
function pluck(obj, keys){
return keys.map(key => obj[key])
}
使用 TypeScript
实现上面这个例子,需要使用 索引类型查询操作符( keyof T )
和 索引访问操作符( T[K] )
。
function pluck<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
return keys.map(key => obj[key])
}
keyof T
T
类型的所有属性名的联合;T[K]
T[K]
属性所对应的值的类型。索引访问操作符
的另一个例子,定义 getProperty()
方法获取对象上某个属性的值:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
keyof T
和 T[K]
可以与字符串索引签名进行交互:如果有一个带有字符串索引签名的类型,那么 keyof T
会是 string
,T[K]
是索引签名的类型:
interface Map<T> {
[prop: string]: T
}
let keys: keyof Map<number> // keys 为 string 类型
let value: Map<number>['foo'] // value 为 number 类型
一个常见的任务是将一个已知类型的所有属性变成可选的:
interface Person {
name: string
age: number
}
type PartialPerson = {
name?: string
age?: number
}
TypeScript
提供了从旧类型中创建新类型的方式 —— 映射类型
:
type Partial<T> = {
[P in keyof T]?: T[P]
}
映射类型
的语法与索引签名类似, 内部使用了 for...in
。
一些通用的 映射类型
(如:Readonly<T>
, Partial<T>
, Pick<T, K extends keyof T>
, Record<K extends string, T>
)被包含进了TypeScript
标准库:
// Readonly<T>
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
// Partial<T>
type Partial<T> = {
[P in keyof T]?: T[P]
}
// Pick<T, K extends keyof T>
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}
// Record<K extends string, T>
type Record<K extends string, T> = {
[P in K]: T
}
其中, Readonly
, Partial
, Pick
是同态的,因为映射只作用于 T
的属性,而 Record
不是同态的:
type ThreeThingRecord = Record<'prop1' | 'prop2' | 'prop3', string>
映射类型
的另一个例子,将 T[P]
包装进 Proxy<T>
中:
type Proxy<T> = {
get(): T
set(value: T): void
}
type Proxify<T> = {
[P in keyof T]: Proxy<T[P]>
}
function proxify<T>(obj: T): Proxify<T> {
let newObj = {} as Proxify<T>
for (const key in obj) {
newObj[key] = getProxyValue(obj, key)
}
function getProxyValue<T, K extends keyof T>(obj: T, key: K): Proxy<T[K]> {
let addProxyValue: Proxy<T[K]> = {
get(): T[K] {
return obj[key]
},
set(value): void {
obj[key] = value
}
}
return addProxyValue
}
return newObj as Proxify<T>
}
let person = {
name: 'Alice',
age: 21
}
let proxifiedPerson = proxify(person)
console.log(person) // { name: 'Alice', age: 21 }
console.log(proxifiedPerson) // { name: { get: [Function: get], set: [Function: set] }, age: { get: [Function: get], set: [Function: set] } }
proxifiedPerson.age.set(22)
console.log(person) // { name: 'Alice', age: 22 }
console.log(proxifiedPerson.age.get()) // 22
了解了如何包装一个类型的属性,那么接下来就是如何拆包。同态映射的拆包很简单:
function unproxify<T>(obj: Proxify<T>): T {
let ret = {} as T
for (const key in obj) {
ret[key] = obj[key].get()
}
return ret
}
let originalPerson = unproxify(proxifiedPerson)
console.log(originalPerson) // { name: 'Alice', age: 22 }
注意这个拆包推断只适用于同态的映射类型。非同态映射需要传入明确的类型参数。