4、类型别名与联合类型:type 关键字、联合类型、交叉类型、字面量类型、类型守卫
好,咱们进入第四讲。这一章的内容,说白了就是 TypeScript 里最常用的「搭积木」技巧。你写 TypeScript 项目,几乎每天都在跟这些东西打交道。我个人习惯把这一章叫做「类型组合拳」——学会了,你就能用基础类型拼出各种复杂结构。
4.1 type 关键字:给类型起个名字
type 是 TypeScript 里定义类型别名的关键字。它的作用很简单:给一个类型起个新名字。为什么要这么做?因为有些类型写起来太长了,或者你会在多个地方重复用到它。
type UserID = string | number;
type Point = {
x: number;
y: number;
};
let id: UserID = "abc123";
let pos: Point = { x: 10, y: 20 };
我在项目中遇到过一种情况:后端返回的用户 ID 有时是字符串,有时是数字。如果每个地方都写 string | number,改起来会疯掉。用 type 一封装,改一处就行。
4.2 联合类型:要么这个,要么那个
联合类型用 | 符号表示。意思就是「可以是类型 A,也可以是类型 B」。你想想看,JavaScript 里很多函数参数本来就是多类型的,比如 document.getElementById 返回 HTMLElement | null。
type Status = "loading" | "success" | "error";
type Result = string | number | boolean;
function printStatus(s: Status) {
console.log(s);
}
printStatus("loading"); // 正确
// printStatus("pending"); // 错误!"pending" 不在联合类型中
嗯,这里要注意:联合类型里的每个成员都是「或」的关系。你传的值只要匹配其中一个就行。我曾经在写一个表单状态机时,把所有可能的状态都列成联合类型,编译时就能发现拼写错误,省了不少调试时间。
4.3 交叉类型:既要这个,又要那个
交叉类型用 & 符号表示。它的意思是「同时满足类型 A 和类型 B」。说白了就是把多个类型合并成一个。
type HasName = { name: string };
type HasAge = { age: number };
type Person = HasName & HasAge;
const p: Person = {
name: "张三",
age: 30
};
交叉类型在合并对象时特别有用。我记得有一次要对接两个不同版本的 API 返回数据,用交叉类型把公共字段和各自特有字段拼在一起,代码干净多了。
{ a: string } & { a: number },结果 a 的类型会变成 never。我曾经因为这个 bug 排查了半天——两个接口的字段名一样但类型不同,交叉后直接没法用。
4.4 字面量类型:精确到具体值
字面量类型就是直接用具体的值作为类型。比如 "hello" 是一个字符串字面量类型,42 是一个数字字面量类型。它们通常跟联合类型搭配使用,用来限定取值范围。
type Direction = "up" | "down" | "left" | "right";
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
function move(d: Direction) {
// ...
}
move("up"); // 正确
// move("forward"); // 错误
为什么需要字面量类型?因为有时候你不想接受任何字符串,只想接受特定的几个。比如按钮的 size 属性,只有 "small" | "medium" | "large" 三种。用字面量类型,IDE 会自动提示,写代码就像在选菜单。
4.5 类型守卫:运行时判断类型
类型守卫就是在运行时检查一个值是不是某个类型,然后 TypeScript 会根据检查结果自动缩小类型范围。常用的有 typeof 和 instanceof。
4.5.1 typeof 类型守卫
typeof 只能检查基本类型:string、number、boolean、symbol、undefined、object、function、bigint。
function formatValue(value: string | number) {
if (typeof value === "string") {
// 这里 value 被收窄为 string
return value.toUpperCase();
} else {
// 这里 value 被收窄为 number
return value.toFixed(2);
}
}
我个人习惯在工具函数里大量使用 typeof 守卫。比如一个格式化函数,传入字符串就大写,传入数字就保留两位小数——这种场景太常见了。
4.5.2 instanceof 类型守卫
instanceof 用来检查对象是不是某个类的实例。它适用于自定义类或者内置类(如 Date、Array)。
class Dog {
bark() { console.log("汪汪"); }
}
class Cat {
meow() { console.log("喵喵"); }
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark(); // 收窄为 Dog
} else {
animal.meow(); // 收窄为 Cat
}
}
嗯,这里要注意:instanceof 检查的是原型链,所以如果两个类有继承关系,父类的 instanceof 检查也会返回 true。我曾经在写一个图形编辑器时,用 instanceof 区分圆形和矩形,结果因为继承关系搞混了,后来改用 discriminated union(可辨识联合)才彻底解决。
type给类型起别名,方便复用- 联合类型
|表示「或」,交叉类型&表示「与」 - 字面量类型精确到具体值,常与联合类型搭配
- 类型守卫(typeof/instanceof)在运行时缩小类型范围
4.6 实战组合:一个完整的例子
咱们把上面这些知识点串起来,写一个实际点的例子。假设你要处理一个 API 响应,可能是成功数据,也可能是错误信息。
// 定义响应类型
type ApiResponse =
| { status: "success"; data: string }
| { status: "error"; code: number; message: string };
// 处理响应
function handleResponse(res: ApiResponse) {
if (res.status === "success") {
// 这里 TypeScript 知道 res 是成功类型
console.log("数据:", res.data);
} else {
// 这里 TypeScript 知道 res 是错误类型
console.error(`错误 ${res.code}: ${res.message}`);
}
}
// 使用
handleResponse({ status: "success", data: "你好" });
handleResponse({ status: "error", code: 404, message: "未找到" });
这个模式叫「可辨识联合」(Discriminated Union),是 TypeScript 里非常经典的设计。每个分支都有一个字面量类型的字段(这里是 status),TypeScript 就能自动帮你收窄类型。我在项目中几乎所有的状态管理都用这个模式,比写一堆 if-else 加类型断言安全多了。
好,这一章就到这里。下一章我们会聊更高级的类型操作——泛型。到时候你会发现,类型系统原来可以这么灵活。