TypeScript 高级类型:交叉类型、联合类型、类型别名、索引类型、映射类型、条件类型。
交叉类型
交叉类型是将多个类型合并为一个类型。
语法为:类型一 & 类型二 — 把
&
理解成and
,
示例
interface Admin {
id: number,
administrator: string,
timestamp: string
}
interface User {
id: number,
groups: number[],
createLog: (id: number) => void,
timestamp: number
}
let t: Admin & User
t!.administrator // 合法 Admin.administrator: string
t!.groups // 合法 User.groups: number[]
t!.id // 合法 id: number
t!.timestamp // 合法 timestamp: never 类型冲突,不可被赋值。
复制代码
应用场景
合并两传入对象的成员属性的例子:
function extend<T, U>(first: T, second: U): T & U {
for(const key in second) {
(first as T & U)[key] = second[key] as any
}
return first as T & U
}
复制代码
函数返回结果的类型是两个对象的交叉类型。调用 extend 函数,实现两个对象的合并:
class Person {
constructor(public name: string) { }
}
class ConsoleLogger {
log() {}
}
let jim = extend(new Person('Jim'), new ConsoleLogger())
let n = jim.name
jim.log()
复制代码
通过 extend()
函数合并了两个类的实例,我们知道交叉类型是 and
的意思,那么合并后即可访问 Person 类实例的 name 属性,也可以调用 ConsoleLogger 类实例的 log() 方法。
联合类型
联合类型表示取值为多种中的一种类型,而交叉类型每次都是多个类型的合并类型。
语法为:类型一 | 类型二 — 把
|
理解成or
let currentMonth: string | number //string 类型 或 number 类型
currentMonth = 'February'
currentMonth = 2
复制代码
联合类型的构成元素除了类型,还可以是字面量:
//布尔字面量
type Scanned = true | false
//对象字面量
type Result = { status: 200, data: object } | { status: 500, request: string}
复制代码
如果一个值是联合类型,那么只能访问联合类型的共有属性或方法。
interface Dog {
name: string,
eat: () => void,
destroy: () => void
}
interface Cat {
name: string,
eat: () => void,
climb: () => void
}
let pet: Dog | Cat
pet!.name // OK
pet!.eat() // OK
pet!.climb() // Error
复制代码
求不同图形面积的综合性实例:
interface Rectangle {
type: 'rectangle',
width: number,
height: number
}
interface Circle {
type: 'circle',
radius: number
}
interface Parallelogram {
type: 'parallelogram',
bottom: number,
height: number
}
// 函数 area() 的参数是一个 Rectangle | Circle | Parallelogram 联合类型
function area(shape: Rectangle | Circle | Parallelogram) {
switch (shape.type) {
case 'rectangle':
return shape.width * shape.height
case 'circle':
return Math.PI * Math.pow(shape.radius, 2)
case 'parallelogram':
return shape.bottom * shape.height
}
}
let shape: Circle = {
type: 'circle',
radius: 10
}
console.log(area(shape))
复制代码
根据其不同的字符串字面量类型引导到不同的 case 分支,这种情况我们称之为可辨识联合(Discriminated Union)
类型别名
类型别名会给类型起个新名字。类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
用关键字
type
定义类型别名。
类型别名不会新建一个类型,而是创建一个新名字来引用此类型。
//原始类型:
type brand = string
type used = true | false
const str: brand = 'imooc'
const state: used = true
//联合类型:
type month = string | number
const currentMonth: month = 'February'
const nextMonth: month = 3
//交叉类型:
interface Admin {
id: number,
administrator: string,
timestamp: string
}
interface User {
id: number,
groups: number[],
createLog: (id: number) => void,
timestamp: number
}
type T = Admin & User
//泛型:
type Tree<T, U> = {
left: T,
right: U
}
复制代码
接口 vs. 类型别名区别
- 接口可以实现 extends 和 implements,类型别名不行。
- 类型别名并不会创建新类型,是对原有类型的引用,而接口会定义一个新类型。
- 接口只能用于定义对象类型,而类型别名的声明方式除了对象之外还可以定义交叉、联合、原始类型等。
类型别名是最初 TypeScript 做类型约束的主要形式,后来引入接口之后,TypeScript 推荐我们尽可能的使用接口来规范我们的代码。
索引类型
索引类型可以让 TypeScript 编译器覆盖检测到使用了动态属性名的代码。
keyof
索引类型查询操作符,获取对象的可访问索引字符串字面量类型
interface User {
id: number,
phone: string,
nickname: string,
readonly department: string,
}
class Token{
private secret: string | undefined
public accessExp: number = 60 * 60
public refreshExp: number = 60 * 60 * 24 * 30 * 3
}
let user: keyof User // let user: "id" | "phone" | "nickname" | "department"
type token = keyof Token // type token = "accessExp" | "refreshExp"
复制代码
对于任何类型 T, keyof T 的结果为 T 上已知的公共属性名的联合
T[K]
索引访问操作符,拿到属性名对应属性值的类型
class Token{
public secret: string = 'ixeFoe3x.2doa'
public accessExp: number = 60 * 60
public refreshExp: number = 60 * 60 * 24 * 30 * 3
}
type token = keyof Token
type valueType = Token[token] // type valueType = string | number
type secret = Token['secret'] // type secret = string
复制代码
一个对象的类型为泛型 T
,这个对象的属性类型 K
只需要满足 K extends keyof T
,即可得到这个属性值的类型为 T[K]
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
return o[name]; // o[name] is of type T[K]
}
复制代码
已知参数 o 的类型为 T,参数 name 的类型 K 满足 K extends keyof T,那么返回值的类型即为 T[K]
函数 pluck()
JavaScript 函数,实现从一个对象中选取指定属性,得到它们的属性值:
function pluck(o, names) {
return names.map(n => o[n])
}
复制代码
改写:
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
return names.map(n => o[n])
}
interface Person {
name: string
position: string
age: number
}
let person: Person = {
name: 'Evan',
position: 'Software Engineer',
age: 27
}
let values: unknown[] = pluck(person, ['name', 'age'])
console.log(values)
复制代码
映射类型
TypeScript 会将一些好用的工具类型纳入基准库中,方便开发者直接使用,本节介绍的映射类型就是这样的工具类型
映射类型可以将已知类型的每个属性都变为可选的或者只读的。
Readonly 与 Partial 关键字
将 Person 接口的每个属性都变为可选属性或者只读属性
interface Person{
name: string
age: number
}
type PersonOptional = Partial<Person>
//等价于
type PersonOptional = {
name?: string
age?: number
}
type PersonReadonly = Readonly<Person>
//等价于
type PersonReadonly = {
readonly name: string
readonly age: number
}
复制代码
源码分析
type Readonly<T> = {
readonly [K in keyof T]: T[K]
}
type Partial<T> = {
[K in keyof T]?: T[K]
}
复制代码
源码使用了映射类型的语法 [K in Keys]
:
- 类型变量
K
:它会依次绑定到每个属性,对应每个属性名的类型。 - 字符串字面量构成的联合类型的
Keys
:它包含了要迭代的属性名的集合。
我们可以使用 for…in 来理解,它可以遍历目标对象的属性
继续分析:
- Keys,可以通过 keyof 关键字取得,假设传入的类型是泛型 T,得到 keyof T,即为字符串字面量构成的联合类型(”name” | “age”)。
- [K in keyof T],将属性名一一映射出来。
- T[K],得到属性值的类型。
已知了这些信息,我们就得到了将一个对象所有属性变为可选属性的方法:
[K in keyof T]?: T[K]
复制代码
进而可得:
type Partial<T> = {
[K in keyof T]?: T[K]
}
复制代码
Pick
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}
interface User {
id: number
age: number
name: string
}
type PickUser = Pick<User, 'id'>
//相当于
type PickUser = { id: number }
复制代码
条件类型
条件类型就是在初始状态并不直接确定具体类型,而是通过一定的类型运算得到最终的变量类型
extends
T extends U ? X : Y
// 若 T 是 U 的子类型,则类型为 X,否则类型为 Y。
// 若无法确定 T 是否为 U 的子类型,则类型为 X | Y
复制代码
示例
declare function f<T extends boolean>(x: T): T extends true ? string : number
const x = f(Math.random() < 0.5) // const x: string | number
// 条件不确定的情况下,得到了联合类型 string | number
const y = f(true) // const y: string
const z = f(false) // const z: number
复制代码
可分配条件类型
在条件类型 T extends U ? X : Y
中,当泛型参数 T
取值为 A | B | C
时,这个条件类型就等价于 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
,这就是可分配条件类型。
可分配条件类型(distributive conditional type)中被检查的类型必须是裸类型参数(naked type parameter)。裸类型表示没有被包裹(Wrapped) 的类型,(如:Array<T>
、[T]
、Promise<T>
等都不是裸类型),简而言之裸类型就是未经过任何其他类型修饰或包装的类型。
应用场景
TypeScript 内置的一些工具类型:
Exclude<T, U>
– 从T
中剔除可以赋值给U
的类型。Extract<T, U>
– 提取T
中可以赋值给U
的类型。NonNullable<T>
– 从T
中剔除 null 和 undefined。ReturnType<T>
– 获取函数返回值类型。InstanceType<T>
– 获取构造函数类型的实例类型。
type T00 = Exclude<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'> // 'b' | 'd'
复制代码
Exclude<T, U>
的实现源码:
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;
复制代码
进阶例子,定义一种方法,可以取出接口类型中的函数类型:
type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>
interface Part {
id: number
name: string
subparts: Part[]
firstFn: (brand: string) => void,
anotherFn: (channel: string) => string
}
type FnNames = FunctionPropertyNames<Part>
//遍历整个接口,然后通过条件类型判断接口的属性值的类型是否是函数类型,
//如果是函数类型,取其属性名。得到
//type FnNames = 'firstFn' | 'anotherFn'
type FnProperties = FunctionProperties<Part>
//工具函数 Pick,拿到这个接口的所有函数类型成员集合
//type FnProperties = {
// firstFn: (brand: string) => void
// anotherFn: (channel: string) => string
//}
复制代码
infer 关键字
在条件类型表达式中,可以在 extends 条件语句中使用 infer 关键字来声明一个待推断的类型变量。
通过 ReturnType 理解 infer
ReturnType<T>
– 获取函数返回值类型
const add = (x:number, y:number) => x + y
type t = ReturnType<typeof add> // type t = number
复制代码
代码解释:
通过 ReturnType 可以得到函数 add() 的返回值类型为 number 类型。但要注意不要滥用这个工具类型,应尽量多的手动标注函数返回值类型。
ReturnType 的实现源码:
/**
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
复制代码
如果 T
满足约束条件 (...args: any) => any
,并且能够赋值给 (...args: any) => infer R
,则返回类型为 R
,否则为 any
类型。
infer
的作用是让 TypeScript
自己推断,并将推断的结果存储到一个类型变量中,infer
只能用于 extends
语句中。
示例:
type T0 = ReturnType<() => string> // string
type T1 = ReturnType<(s: string) => void> // void
type T2 = ReturnType<<T>() => T> // unknown
复制代码
借助 infer 实现元组转联合类型
借助 infer 可以实现元组转联合类型,如:[string, number] -> string | number
type Flatten<T> = T extends Array<infer U> ? U : never
//如果泛型参数 T 满足约束条件 Array<infer U>,那么就返回这个类型变量 U
type T0 = [string, number]
type T1 = Flatten<T0> // string | number
复制代码
元组类型在一定条件下,是可以赋值给数组类型,满足条件:
type TypeTuple = [string, number]
type TypeArray = Array<string | number>
type B0 = TypeTuple extends TypeArray ? true : false // true
复制代码