TypeScript 联合类型
联合类型是什么
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 的局限性
- 不支持 string、number、boolean ...
- 不支持 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 | ...
- A、B、C、D... 有相同属性 kind或其它
- kind 的类型是 简单类型
- 各类型中的 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 // 报错 无法赋值
}
重新认识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
的真实名字应该叫做 noErrorType
,unknown
就是所有类型的联合。
这就是为何之前用 unknown
的时候,要使用 as
,as
后面的东西就是要收窄的范围。
注意: 你也可以认为这个角度是是错的,只要能够自圆其说就可以。
转载自:https://juejin.cn/post/7151207932913254431