交叉类型

交叉类型将多个类型合并为一个类型,新类型拥有所有类型的特性。使用 & 符号连接多个类型。

基本语法

交叉类型使用 & 符号连接多个类型。

interface Name {
  name: string
}

interface Age {
  age: number
}

type Person = Name & Age

const person: Person = {
  name: "张三",
  age: 25
}

console.log(person.name)
console.log(person.age)

Person 类型必须同时具有 nameage 属性。

合并多个类型

可以合并任意数量的类型。

interface Name {
  name: string
}

interface Age {
  age: number
}

interface Email {
  email: string
}

type User = Name & Age & Email

const user: User = {
  name: "张三",
  age: 25,
  email: "zhangsan@example.com"
}

console.log(user)

User 类型合并了三个接口的所有属性。

交叉类型与函数

交叉类型可以合并函数签名。

type Log = (message: string) => void
type Send = (message: string) => boolean

type LogAndSend = Log & Send

const logAndSend: LogAndSend = (message: string) => {
  console.log(message)
  return true
}

logAndSend("发送消息")

合并后的函数类型需要同时满足两个签名的要求。

交叉类型与类型别名

交叉类型常与类型别名一起使用。

type ID = string | number
type WithTimestamp = {
  timestamp: number
}

type Entity = {
  id: ID
} & WithTimestamp

const entity: Entity = {
  id: "user-001",
  timestamp: Date.now()
}

console.log(entity)

Entity 类型有 idtimestamp 两个属性。

属性冲突

当合并的类型有相同属性但类型不同时,会导致问题。

interface A {
  value: string
}

interface B {
  value: number
}

type C = A & B

const c: C = {
  value: "hello" as never
}

value 属性同时需要是 stringnumber,这实际上是不可能的,类型变成了 never

方法合并

相同方法的合并会形成重载。

interface Document {
  createElement(tag: string): HTMLElement
}

interface Document {
  createElement(tag: "canvas"): HTMLCanvasElement
  createElement(tag: "div"): HTMLDivElement
}

const doc: Document = document

const canvas = doc.createElement("canvas")
const div = doc.createElement("div")

合并后的 createElement 方法有多个重载签名。

实用场景

交叉类型常用于混入模式。

interface Serializable {
  serialize(): string
}

interface Loggable {
  log(): void
}

function applyMixins<T extends Serializable & Loggable>(obj: T): void {
  obj.serialize = function() {
    return JSON.stringify(this)
  }
  obj.log = function() {
    console.log(this.serialize())
  }
}

class User implements Serializable, Loggable {
  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }

  serialize!: () => string
  log!: () => void
}

const user = new User("张三", 25)
applyMixins(user)
user.log()

混入模式通过交叉类型实现代码复用。

与联合类型的区别

交叉类型与联合类型是相反的概念。

interface A {
  a: string
}

interface B {
  b: number
}

type Intersection = A & B
type Union = A | B

const intersection: Intersection = {
  a: "hello",
  b: 123
}

const union1: Union = { a: "hello" }
const union2: Union = { b: 123 }
const union3: Union = { a: "hello", b: 123 }

交叉类型需要满足所有类型,联合类型只需满足其中一个。

实际应用

交叉类型在实际开发中常用于扩展现有类型。

interface BaseResponse {
  code: number
  message: string
}

interface UserData {
  id: number
  name: string
}

type UserResponse = BaseResponse & {
  data: UserData
}

async function fetchUser(id: number): Promise<UserResponse> {
  return {
    code: 200,
    message: "success",
    data: {
      id,
      name: "张三"
    }
  }
}

async function main() {
  const response = await fetchUser(1)
  console.log(response.data.name)
}

main()

UserResponse 扩展了 BaseResponse,添加了 data 属性。

小结

交叉类型使用 & 符号合并多个类型,新类型拥有所有类型的特性。可以合并任意数量的类型,但相同属性的不同类型会导致冲突。交叉类型常用于混入模式和扩展现有类型。与联合类型相反,交叉类型需要满足所有类型的要求。