【Typescript小小册】高级类型
概述
Typescript 的高级类型指操作基本类型和复合类型而得的类型,包括:
- 联合
- 交叉
- 泛型
类型别名
在讲具体的高级类型之前,我们先了解一下 Typescript 的类型别名。类型别名也是一种类型,用一个单词代表可能比较复杂的类型声明,用关键字type表示。
示例:
type S = string
let a: S = 'a'
这里用S作为string的别名,使用方法和string一模一样。
别名不仅可以代表基本类型,它可以代表任意类型。示例:
type SA = string[] // 代表字符串数组
type Handler = (x: string) => void // 代表函数
type I = {
// 代表接口
name: string
value: number
}
class C {
name: string = 'a'
value: number = 0
}
type D = C // 代表类
类型
联合
联合类型是指变量为多个类型中的一种,是“或”的关系,用操作符|表示。
示例:
type StringOrNumber = string | number
let a: StringOrNumber = 'a'
let b: StringOrNumber = 0
function log(x: string | number) {
console.log(x)
}
这里StringOrNumber表示一种可以是字符串或者也可以是数字的类型,所以对于StringOrNumber类型的变量,既可以赋值为字符串,也可以赋值为数字。
在 Typescript 中,null和undefined与其它类型是同层级的,它们不是任何类型的子类型,也就是说我们不能用它们来表示一个特定类型的空指针。但是我们确实需要在一个变量未确定时保持为空状态,这时可以用联合类型来表示一个变量可为null或undefined。示例:
interface A {
name: string
}
let a: A | null = null
字面量类型
Typescript 中字面量也可以作为一个类型。比如:
let a: 'A'
上述声明表示变量a的值只能是'A',赋值为其它值时会报错。
数字和布尔也可以作为字面量类型。这看起来没什么用,但是这种特性一般会与联合结合起来使用。比如:
type Option = 'A' | 'B' | 'C' | 'D'
let a: Option = 'A'
let e: Option = 'E' // 错误
Option类型的变量只能被赋值为'A'、'B'、'C'、'D'其中一个值。这一种用法和枚举很像,比如:
enum Option {
A,
B,
C,
D
}
不同的是,字面量类型本质上是字符串,可以使用字符串的所有方法。而且,通过keyof操作符,我们可以动态的生成字面量类型,这一点将在之后介绍。
交叉
交叉类型是指多个类型合并成一个类型,是“且”的关系,用操作符&表示。
示例:
interface A {
x: string
y: number
}
interface B {
x: string
z: number
}
type C = A & B
let c: C = { x: 'x', y: 0, z: 1 }
交叉类型的成员包含了所有原类型的的所有成员,比如上述代码中,c变量必须既是A类型也是B类型,也就是说它必须同时拥有两个类型要求的属性。
有一些类型是无法交叉的,比如基本类型。示例:
type A = string & number
因为一个变量不可能既是字符串又是数字,所以最终A类型是never类型。
如果被交叉的两个类型有同名但类型不同的成员,那么这两个同名成员也会被交叉。示例:
interface A {
x: { a: string; b: number }
}
interface B {
x: { a: string; c: number }
}
type C = A & B
let c: C = { x: { a: 'a', b: 0, c: 1 } }
类型A和B有同名但类型不同的成员x,交叉时两个x成员也可以交叉,因此最终x的类型为:{a: string, b: number, c: number}。
泛型
Typescript 中的泛型与其它面向对象语言中的泛型和相似,包括:泛型函数、泛型类和泛型接口。
泛型函数
示例:
function merge<T, U>(x: T, y: U): { x: T; y: U } {
let t: { x: T; y: U } = { x, y }
return t
}
merge<string, number>('a', 0)
我们可以在中括号内声明函数中使用到的泛型变量,它代表了在调用函数时传入的类型。类型变量T可以用在任何需要类型声明的地方,比如参数类型、返回值类型、局部变量类型等。
箭头函数也可以包含泛型。示例:
const merge = <T, U>(x: T, y: U): { x: T; y: U } => {
let t: { x: T; y: U } = { x, y }
return t
}
泛型类
示例:
class Merge<T, U> {
x: T
y: U
constructor(x: T, y: U) {
this.x = x
this.y = y
}
merge(): { x: T; y: U } {
let t: { x: T; y: U } = { x: this.x, y: this.y }
return t
}
}
let merge = new Merge<string, number>()
我们可以在中括号内声明类中使用到的泛型变量,它代表了在实例化类时传入的类型。类型变量T可以用在任何需要类型声明的地方,比如成员、构造方法参数类型等。
泛型接口
示例:
interface Merge<T, U> {
x: T
y: U
}
let merge: Merge<string, number> = { x: 'a', y: 0 }
我们可以在中括号内声明接口中使用到的泛型变量,它代表了在声明变量类型时传入的类型。类型变量T可以用在任何需要类型声明的地方,比如成员、方法参数类型等。
泛型约束
由于在泛型类型中,泛型变量是在使用时传入的,所以在编译时无法得知其具体类型,那么读取类类型变量的任何成员都是不行的。比如:
function scale<T>(x: T): number {
return x.length // 错误
}
虽然我们可能心里知道在使用时我们会传入一个具有length成员的变量(比如字符串),但是在编译时编译器并不知道。因此,我们需要告诉编译器T类型是那一类具有length成员的类型。使用extends关键字:
interface HasLength {
length: number
}
function scale<T extends HasLength>(x: T): number {
return x.length
}
scale('abc')
我们使用extends对类型变量T进行了约束,它必须具有length属性。也就是是说T类型必须实现或继承了 HasLength 类型。