TS 中的类型运算:联合类型

TS = JS + 类型系统

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

联合类型(并集)

英文全称:union types

type A = { name: string } 表示 namestring 的所有对象
但不要错误地认为这些对象只有 name 这一个 key
比如对象 { name: 'ClariS'; age: 18 } 也是属于 A 类型的
A 类型的对象可以有 age,也可以没有 age
同理,type = { age: number } 表示 agenumber 的对象,这些对象的 name 可以为空,也可以不为空

如何使用联合类型?

1
2
3
4
5
const f1 = (a: number | string) => {
'既不能把 a 当作 number'
'也不能把 a 当做 string'
'那么,怎么使用 a 变量呢?'
}

如果不拆开类型,就只能使用 numberstring 共同拥有的方法或属性,比如 toString()

因此要使用联合类型,就得先区分类型,也就是进行类型收窄(Narrowing)

如何进行类型收窄?

使用 typeof

1
2
3
4
5
6
7
8
9
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');
}
};

typeof 的局限性:无法区分数组、日期、普通对象以及 null(返回值都是 "object"

使用 instanceof

1
2
3
4
5
6
7
8
9
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 的局限性:

  • 不支持基础类型,stringnumberbigintbooleansymbolundefined
  • 不支持 TS 独有的类型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    type Person = { name: string };

    const f1 = (a: Person | Person[]) => {
    if (a instanceof Person) {
    // ^--- 报错:type 不能用作 value
    } else {
    throw new Error('Never do this');
    }
    };

使用 in 操作符

1
2
3
4
5
6
7
8
9
type Person = { name: string };

const f1 = (a: Person | Person[]) => {
if ('name' in a) {
a; // Person
} else {
a; // Person[]
}
};

in 操作符的局限性:只适用于部分对象

比如当判断的两个对象存在相同的 key (存在相同 key,其中一个对象的 key 可能不存在),或不为键值对形式的对象(比如日期、正则、函数等)时,则无法使用 in 操作符来收窄类型

使用 JS 中判断类型的函数

1
2
3
4
5
6
7
8
9
const f1 = (a: string | string[]) => {
if (Array.isArray(a)) {
a.join('\n').toString();
} else if (typeof a === 'string') {
parseFloat(a).toFixed(2);
} else {
throw new Error('Never do this');
}
};

使用逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const f1 = (a?: string[]) => {
if (a) {
a; // string[]
} else {
a; // undefined
}
};

const f2 = (a: 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 | number
}
};

使用类型谓词 is

上述所有的类型收窄方法都是通过 JavaScript 来实现的,有没有区分类型的万全之法?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Rect = { height: number; width: number };

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

// 推荐使用普通函数实现 isRect,而不使用箭头函数
function isRect(x: Rect | Circle): x is Rect {
return 'height' in x && 'width' in x;
}

const f1 = (a: Rect | Circle) => {
if (isRect(a)) { // `isRect(a)` 判断 a 是不是 Rect
a; // Rect
} else {
a; // Circle
}
};

优点:支持所有 TS 类型
缺点:需要自己实现,挺麻烦的

有没有更简单的办法?

可辨别联合

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Circle { kind: "circle"; radius: number; }
interface Square { kind: "square"; sideLength: number; }
type Shape = Circle | Square

const f1 = (shape: Shape) => {
if (shape.kind === 'circle') {
shape // Circle
} else if (shape.kind === 'square') {
shape // Square
} else {
shape // never
}
}

让复杂类型的收窄变成简单类型的对比

对于类型 T = A | B | C | D | ...,有以下要求:

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

则称 T可辨别联合

一句话总结:具有同名、可辨别的简单类型的 key 的联合类型,称为可辨别联合

使用断言

可以使用 as 进行强制的类型收缩

1
2
3
4
5
6
7
interface Circle { kind: "circle"; radius: number; }
interface Square { kind: "square"; sideLength: number; }
type Shape = Circle | Square

const f1 = (shape: Shape) => {
(shape as Circle).radius = 4
}

any 等于所有类型的联合吗?

这里直接给出结论:any 不等于所有类型的联合

注意:这里的所有类型不包括 never unknown any void

用反证法可以证明:

  1. 只要类型发生了联合,就只能使用它们共同拥有的属性或方法(想要使用各自的方法就必须做类型收窄)
  2. 但是使用 any 之后,不做类型收窄可以使用所有类型的方法
  3. 说明 any 不是所有类型的联合
1
2
3
4
5
6
7
8
9
10
11
const f1 = (a: string | number) => {
a.toString();
a.toFixed(); // 报错
a.split(); // 报错
};

const f2 = (a: any) => {
a.toString();
a.toFixed();
a.split();
};

且 ts 的绝大部分规则对 any 不生效,但有一种特殊情况,any 无法赋值给 never

那什么等于所有类型的联合呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const f1 = (a: unknown) => {
if (a instanceof Date) {
a; // Date
}
};

type Rect = { height: number; width: number };
type Circle = { center: [number, number]; radius: number };
function isRect(x: Rect | Circle): x is Rect {
return 'height' in x && 'width' in x;
}

const f2 = (a: unknown) => {
if (isRect(a)) {
a; // Rect
}
};

可以看出,unknown 是可以收窄到任何类型的,因此,unknown 就是所有类型的联合


(●'◡'●)ノ♥