TypeScript 联合类型

594 阅读8分钟

我正在参加「掘金·启航计划」

联合类型是什么

TS = JS + 类型系统

JS 可以对 进行加减运算,如果把 TS 的类型系当作一门语言,TS 可以对类型 进行各种运算

TS 类型系统,有哪些运算 ?

联合类型(并集)union types

type A1 = number
type B1 = string
type C1 = A1 | B1  // 表示把 A1 和 B1 加起来
const c1: C1 = 42  // 可以在 A1 和 B1 集合里面随便选一个值

type A2 = { name: string }
type B2 = { age: number }
type C2 = A2 | B2 // 表示 A2 和 B2 的并集
const c2: C2 = {  // 这里的举的例子正好在交集部分
  name: 'John',
  age: 18
}

type A2 = {name: string} 表示 name 为 string 的所有对象,但是不要错误地以为这些对象只有 name 这一个 key ,比如对象 {name: "xxx", age: 18} 也是属于 A2 类型的,即 A2 类型的对象可以有 age,也可以没有 age。

同理, type B2 = {age: number}表示 age 为 number 的对象,这些对象的 name 可以为空,也可以不为空

如何使用联合类型 ?

const f1 = (a: number | string) => {
  "即不能把 a 当做 nunmber"
  "也不能把 a 当做 string"
  "那么,怎么使用 a 变量呢"
}

答:想办法把类型区分开来

你不拆开就只能使用 number 和 string 同时拥有的方法和属性,如 toString()

// 我们声明了联合类型之后,一定会把它们拆开,这个拆开的过程叫做类型收窄(Narrowing)
const f1 = (a: number | string) => {
  if ( typeof a === 'number' ) {
    a.toFixed(2)
  } else {
    a.split(',')
  }
}

用JS做类型收窄

使用 typeof 来区分类型

typeof 做类型判断返回的字符串,只有以下几种可能

"string" "number" "bigint" "boolean"
"symbol" "undefined" "object" "function"

问题在于有些东西它没有返回,比如 "null" typeof 做类型判断的时候返回的是 "object"

const f1 = (a: null | {name: string}) => {
  if (typeof a === 'null') { // 这里就不可以,也不允许使用
  }
}

typeof 没有办法对所有的类型都进行区分,只能区分它能区分的

typeof 的局限性

typeof 数组对象、 typeof 普通对象、 typeof 日期对象、 typeof null 做类型判断的时候返回的都是 "object"

使用 instanceof 来区分类型

instanceof 返回的数据类型主要是以对象的类为主的

// 下面的拆开过程也叫类型收窄
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')
  }
}

instanceof 的局限性

  1. 不支持 string、number、boolean ...
  2. 不支持 TS 独有的类型

对于第一个缺点我们可以写成如下代码

// 结合 instanceof 和 typeof 配合在一起用,可以解决大部分问题
const f1 = (a: Date | string | Date[]) => {
  if(typeof a === 'string' ) {
    a
  } else if ( a instanceof Date) {
    a
  } else {
    a
  }
}

但是对于第二个缺点是无论如何也解决不了的,无法解决 TS 独有的类型,就是会被擦除的类型

type Person = {
  name: string
}
type Animal =  {
 x: string
}

// 能否区分它是 Persoon 还是 Animal ?
const f1 = (a: Person | Animal) => {
  if (typeof a === "string"){  // 这里必然得到的是对象,它们会被类型擦除
    a
  } else if ( a instanceof Person) {   // 会报错,Person 只能用来表示类型,但你却把它当做一个class值
    // 以上两个东西它都不是类,就没有实例的概念
    a
  } else {
    a
  } 
}

使用 in 来收窄类型

当 instanceof 和 type 这两种都不能用的话,可以使用 in

// 对于对象,看 key, JS 虽然不知道你是什么类,但是我知道你用了什么 key
type Person = {
  name: string
}
type Animal =  {
  x: string
}

const f1 = (a: Person | Animal) => {
  if("name" in a) {
    a // 类型是 Person
  } else if('x' in a) {
    a // Animal
  } else {
    a
  }
}

但是它只适用于部分对象

使用 JS 中判断类型的函数来区分

const f1 = (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')
  ]
}

这种方法的局限就是你依然无法支持会被擦除的类型,而且有些类型 JS 没有提供判断的函数

此方法的局限性是什么?

我们一直在尝试使用 JS 判断类型的逻辑来判断 TS 的里面的逻辑,对于 JS、TS 它们是无法做到一一对应的,这是两门语言。

使用逻辑来收窄类型

const f1 = (a?: string[]) => {  // 假设 a 要么是空要么是一个数组
  // 通过逻辑来进行类型收窄
  if(a){
    a // string[]
  }else{
    a // undefined
  }
}

const f2 = (b: string |number) => {
  a = 1  // 通过赋值来猜测它的类型
  a // number
}

const f3 = (x: string | number, y: string | boolean) => {
  if(x === y) {
    x // string
    y // string
  } else {
    x // string | number
    y // string | boolean
  }
}

但是很多情况是无法推断的

以上所有类型收窄都是通过 JS 来实现的,总是会有缺陷,那么有没有区分类型的万全之法?

有两种方法: 类型谓词 is、可辨别联合

类型谓词/类型判断 is

使用类型谓词 is 来判断任意类型的收窄

type Rect = { height: number; width: number }
type Circle = { center: [number, number]; radius: number }

// 这个方法专门用来判断 x 是不是矩形
function isRect(x: Rect | Circle): x is Rect { 
  // 这里的 可以随意发挥 可以使用 in typeof instanceof
  return 'height' in x && 'width' in x
}

const f1 = (a: Rect | Circle) => {
  if(isRect(a)) {
    a // Rect
  } else {
    a // Circle
  }
} 
// 如果 写成 boolean
function isRect(x: Rect | Circle): boolean { 
  return 'height' in x && 'width' in x
}

// isRect(a) 只有 true/false 就无法判断类型
// 所以必须把含义明确一下 这个 boolean 表示 x 是不是 Rect

is 的优点:

  • 支持所有 TS 类型

is 的缺点

  • 麻烦

可辨别联合 Discriminated Unions

如何使用 a.kind 区分 a 的类型

// 一段很傻的代码
const f1 = (a: A | B) => {
  if (a.kind === 'A') {
    a // A
  } else if (a.kind === 'B') {
    a // B
  } else {
    a //never
  }
}

type A = { kind: 'string'; value: string }
type B = { kind: 'number'; value: number }
// 第一步
type Circle = {center: [number, number]}
type Square = {sideLength: number}
type Shape = Circle | Square

// 之前要写两个 is
const f1 = (a: Shape) => {
  
}
// 第二步
// 分别在两个对象上加 kind
type Circle = {kind: 'Circle', center: [number, number]}
type Square = {kind: 'Square', sideLength: number}
type Shape = Circle | Square

// 之前要写两个 is
const f1 = (a: Shape) => {
  if(a.kind === 'Circle') {
    a
  } else {
    a
  }
}
// 第三步 只需要简单的加上一个字段就能解决类型收窄的问题
type Circle = {kind: 'Circle', center: [number, number]}
type Square = {kind: 'Square', sideLength: number}
type Shape = Circle | Square

// 那么要同时排除 string number 呢?
const f1 = (a: string | number | Shape) => {
  if(typeof a === 'string') {
    a
  } else if (typeof a === 'number') {
    a
  }else if(a.kind === 'Circle') {
    a
  } else {
    a
  }
}
// 为和不用 in 来区分?
// 情况 4
type Circle = {kind: 'Circle', x?: [number, number]}
type Square = {kind: 'Square', x: number}
// 以上无法用 in 来区分了

type Shape = Circle | Square

const f1 = (a: string | number | Shape) => {
  if(typeof a === 'string') {
    a
  } else if (typeof a === 'number') {
    a
  }else if(a.kind === 'Circle') {
    a
  } else {
    a
  }
}

这个 kind 可以换成任意名,可辨别联合的优点:

  • 复杂类型(对象)收窄 变成 简单类型对比

要求: T = A | B | C | D | ...

  1. A、B、C、D... 有相同属性 kind或其它
  2. kind 的类型是 简单类型
  3. 各类型中的 kind 可区分

则称 T可辨别联合

注意:得要先有需求再加 kind

总结

可辨别联合: 同名、可辨别的简单类型的 key

内容回顾

类型是可以联合的 |, 联合起来怎么去用? 必须要类型收窄,那么就要对类型进行区分:

  • JS 方法: typeof x 、in 、instanceof、isArray()、 判空 ... (每个方法都有局限性)

想到统解的方法

  • TS 方法: is 类型谓词
  • TS 方法: x.kind 可辨别联合
  • TS 方法: 断言
// 断言
type Circle = {kind: 'Circle', center: [number, number]}
type Square = {kind: 'Square', x: number}

type Shape = Circle | Square

const f1 = (a: Shape) => {
  // 自己把 a 当做 Circle 来用, 强制的进行收窄
  (a as Circle).center
}

f1({kind: 'Circle', center: [1, 2]})

什么时候用 可辨别联合类型 ?

如果在写 React

type Action = {type: 'getUser', id: string}
             | {type: 'createUser', attributes: any}
             | {type: 'deleteUser'. id: string}
             | {type: 'updateUser', attributes: any}

思考题

法外狂徒 any

any 类型是否等于所有类型的联合吗?为什么?

这个所有类型的前提是除了 never/unknown/any/void

因为只要你做了联合,这个类型就不能用

const f1 = (a: string | number) => {
  a.  // a 一旦做了联合类型 a 就不能用了,只能用 这两个类型都包含的方法
}
// 但是用 any 就不会报错

所以用反证法可以证明: any 不等于所有类型的联合,因为一旦类型联合起来,我们就只能使用它交集中的东西,除非我们做类型区分/类型收窄才能使用每个类型的方法。但是当我用 any 之后,发现我可以使用所有类型的方法。

官网说 如果你想要禁用类型检查你就用 any

TS 绝大部分 规则对 any 不生效, any 就是法外狂徒

// any 会生效的
const f1 = (a: any) => {
  const b: never = a  // 报错 无法赋值
}

Screen Shot 2022-10-06 at 9.18.00 AM.png

重新认识unknown

继续上一问:那么什么东西等于所有类型的联合?

type A = { name: string, age: number, gender: string }
type B = { name: string, xxx: number }

const f1 = (a: A | B) => {
  a.name  // 这里只有 name 被提示出来了,只能用共有方法
}

答案:unknown

const f1 = (a: unknown) => {
  if (a instanceof  Date) {
    a // Date
  } else if (type a === 'string') {
    a // string
  }
  
  // 每次可以选择其中一个类型
  if (typeof === 'number') {
    a // number
  }
}

const f2 = (a: unknown) => {
  if (isPerson(a)) {
    a // Person
  }
}

以上案例说明: unknown 类型 是可以收窄到任何类型的,反过来说 unknown 其实就是任何类型联合起来了,你必须通过收窄才能选择其中一个用,而且每次只能选择其中一个。

所以 any 的真实名字应该叫做 noErrorTypeunknown 就是所有类型的联合。

这就是为何之前用 unknown 的时候,要使用 asas 后面的东西就是要收窄的范围。

注意: 你也可以认为这个角度是是错的,只要能够自圆其说就可以。