TypeScript | 高级类型

587 阅读8分钟

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

学习链接