TypeScript中的联合类型、交叉类型和类型兼容

659 阅读5分钟

1. 联合类型

TS可以对类型进行各种操作,包括与(&)、并(|)等。

1.1 联合类型描述

// 联合类型 Union Type

type A2 = { name: string }
type B2 = { age: number }

type C2 = A2 | B2
const c2: C2 = {
  name: 'lwz',
  age: 18,
}
console.log(c2)

// 在声明的使用是联合类型,而在使用的时候是类型收缩(收窄narrowing)
const f1 = (a: number | string) => {
  if (typeof a === 'number') {
    a.toFixed(2)
  } else if (typeof a === 'string') {
    parseFloat(a).toFixed(2)
  } else {
    throw new Error('Never do this')
  }
}

1.2 类型收窄

  • typeof
/**
 * typeof 只能返回如下几种类型:
 * string/number/bigInt/boolean/symbol/undefined
 * object/function
 * can't return null
 * limitation:
 * typeof 数组对象
 * typeof 普通对象
 * typeof 日起对象
 * typeof null
 * 以上四种类型 typeof的返回结果统一是object
 */
  • instanceof
/**
 * 使用 instanceof 来区分类型
 * limitation
 * 1. 不支持基本类型判断
 * 2. 不支持TS独有的类型
 * 3. 使用 typeof 和 instanceof 可以解决大部分问题
 * 4. 当使用上面方法无法判断时,可以使用in方法判断
 */
 const f1 = (a: Array<Date> | Date) => {
  if (a instanceof Date) {
    a.toISOString()
  } else if (a instanceof Array) {
    a[0].toISOString()
  } else {
    throw new Error('Never do this')
  }
}

const f2 = (a: Date | string | Date[]) => {
  if (typeof a === 'string') {
    a.toLocaleLowerCase()
  } else if (a instanceof Date) {
  } else {
  }
}
  • in
type Person = {
  name: string
}
type Animal = {
  x: string
}

const f3 = (a: Person | Animal) => {
  if ('name' in a) {
  } else if ('x' in a) {
  } else {
  }
}
  • ts自动推导


// 以上几种判断方法都是使用JS中判断类型的函数来区分
const f4 = (a: string | string[]) => {
  if (Array.isArray(a)) {
    a.join('\n').toString()
    // 此处 a 的类型是 string[]
  } else if (typeof a === 'string') {
    parseFloat(a).toFixed(2)
  } else {
    throw new Error('Never do this')
  }
}

// ts自动推导功能
const f5 = (x: string | number, y: string | boolean) => {
  if (x === y) {
    x // string
    y // string
  } else {
    x // string | number
    y // string | boolean
  }
}
  • 类型谓词is
type Rect = {
  height: number
  width: number
}

type Circle = {
  center: [number, number]
  radius: number
}

const f1 = (a: Rect | Circle) => {
  if (isRect(a)) {
    a
  }
}

function isRect(x: Rect | Circle): x is Rect {
  return 'height' in x && 'width' in x
}

// 使用箭头函数会报错
// const isRect(x: Rect | Circle): x is Rect => {
//   return 'height' in x && 'width' in x
// }

/**
 * is 优点:
 * 可以声明所有的类型
 * 缺点:
 * 麻烦, 需要写好多东西
 */

  • 可辨别类型
type A = { kind: 'string'; value: string }
type B = { kind: 'number'; value: number }

const f1 = (a: A | B) => {
  if (a.kind === 'string') {
    a
  } else {
    a
  }
}

type Circle = { kind: 'Circle'; center: [number, number] }
type Square = { kind: 'Square'; sideLength: number }
type Shape = Circle | Square

const f2 = (a: string | number | Shape) => {
  // 首先使用typeof或者instanceof排除掉简单类型
  if (typeof a === 'string') {
    a
  } else if (typeof a === 'number') {
    a
  } else if (a.kind === 'Circle') {
    a
  } else {
    a
  }
}

/**
 * 优点:
 * 让复杂类型的收窄变成简单类型的对比
 * 要求:
 * 1. A, B, C, D... 有相同属性kind或者其他category
 * 2. kind的类型是简单类型
 * 3. 各类型中的kind 可以有交集,但是需要包含各自特有的属性
 * 具有以上特点的 T 称为 可辨别类型
 * 一句话总结: 同名且可辨别的简单类型的key
 */
  • 法外狂徒any
// 思考: any 是所有类型的联合吗? 为什么
// 答案不是 (反证法)
// 联合类型一旦排除某个类型后就不能使用其他类型的方法
// 而any 是可以使用所有的方法

// any = 法外狂徒
// TS 绝大部分 规则对any不生效

// const f1 = (a: any) => {
//   const b: never = a
// }
// 什么 = 所有类型(除了 never/unknown/any/void)的联合? 为什么
// unknown 答案见下
  • 重新认识unknown
const f1 = (a: object) => {
  if (typeof a === 'string') {
    a
  }
}

// const f2 = (a: unknown) => {
//   if (typeof a === 'string') {
//     a
//   } else if (typeof a === 'number') {
//     a
//   }
// }

// type isPerson = {
//   name: string
//   age: number
// }
// const f3 = (a: unknown) => {
//   if (a instanceof Date) {
//     a
//   } else if (isPerson(a)) {
//   }
// }

2. 交叉类型

  • 交叉类型并集
type 有左手的人 = {
  left: string
}

// 这种写法没有问题
// const a: 有左手的人 = {
//   left: 'yes'
// }

// 这种写法有问题
// const a: 有左手的人 = {
//   left: 'yes',
//   right: 'yes'
// }

// 修改写法
const b = {
  left: "yes",
  right: "yes"
}

const a: 有左手的人 = b
console.log(a)
  • 交叉类型补充
type Person = {
  id: string
  name: string
}

type User = {
  id: number
  email: string
} & Person

const a: User = {
  name: "lwz",
  email: "x",
  id: 1 as never
}

console.log(a.id)

// interface Person {
//   id: string
//   name: string
// }

// 当对外提供接口的时候,为方便扩展,此时使用interface要比使用type方便
// interface在声明的时候就会报错,而type只有在编译的时候才会报错
// interface User extends Person {
//   id: number
//   email: string
// }

// const a: User = {
//   name: "lwz",
//   email: "x",
//   // 此时id没有意义
//   id: 1 as never
// }

// console.log(a.id)

type A = {
  method: (n: number) => void
}
type B = {
  method: (n: string) => void
} & A

const b: B = {
  method: n => {
    console.log(n)
  }
}
console.log(b.method(1))

// 函数的交集会得到一个参数的并集
type F1 = (n: number) => void
type F2 = (n: string) => void
type X = F1 & F2
const x: X = n => {
  console.log(n)
}
console.log(x("abc"))

// 交叉类型常用于有交集的类型A、B
// 如果A、B无交集 1) 可能得到never,也可能只是属性为never

3. 类型兼容

3.1 为什么需要类型兼容

const config = {
  a: "111",
  b: "222",
  c: "333",
  d: "444"
}

// const returnCg = config => lodash.pick(config, ["a", "b", "c"])
// returnCg(config)
// 当我们只需要三个值的时候,此时传了4个值,这种情况下需要类型兼容

3.2 简单类型和普通对象如何兼容

type A = string | number

// 类型小的可以代替类型大的
const a: A = "hi"

// 获取类型 typeof
// 当需要给user的对象声明一个类型的时候用到typeof
let user = {
  name: "1",
  age: 18,
  id: 1,
  email: "xxx"
}

// type User = typeof user

// 限制少的可以代替限制多的
type Person = {
  name: string
  age: number
}

type User = {
  name: string
  age: number
  id: number
  email: string
}

const u: User = {
  name: "lwz",
  age: 18,
  id: 1001,
  email: "xxx"
}
const p: Person = u

// User 作为参数也可以传递,此种情况下相当于多传了但没有用到值

3.3 接口继承

// 当两个接口没有父子继承关系的时候也是支持类型兼容的

interface 有左手的人 {
  left: string
}

interface 有双手的人 {
  left: string
  right: string
}

let person: 有双手的人 = {
  left: "yes",
  right: "yes"
}

let personLeft: 有左手的人 = person

3.4 函数参数兼容

interface MyEvent {
  timestamp: number
}
interface MyMouseEvent extends MyEvent {
  x: number
  y: number
}
function listenEvent(eventType: string, handler: (n: MyEvent) => void) {
  /* ... */
}
// 我们希望这样用
// tsconfig.json 中配置 strictFunctionTypes: false
listenEvent("click", (e: MyMouseEvent) => console.log(e.x + "," + e.y))
// 但只能这样用
listenEvent("click", (e: MyEvent) => console.log((e as MyMouseEvent).x + "," + (e as MyMouseEvent).y))
// 还可以这么用
listenEvent("click", ((e: MyMouseEvent) => console.log(e.x + "," + e.y)) as (e: MyEvent) => void)
// 这个就太离谱了
listenEvent("click", (e: number) => console.log(e))

3.5 函数返回值兼容

let 返回值属性少集合大 = () => {
  return { name: "Alice" }
}

let 返回值属性多集合小 = () => {
  return { name: "Alice", location: "Seattle" }
}

// OK
返回值属性少集合大 = 返回值属性多集合小

// 报错:'location' is missing
返回值属性少集合小 = 返回值属性多集合大

3.6 JSON类型值

type JSONValue = string | number | null | boolean | JSONValue[] | { [k: string]: JSONValue }