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 一封装,改一处就行。

小技巧:type 的名字建议用大驼峰(PascalCase),跟普通变量区分开。这是社区惯例,看着也舒服。

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 会根据检查结果自动缩小类型范围。常用的有 typeofinstanceof

4.5.1 typeof 类型守卫

typeof 只能检查基本类型:stringnumberbooleansymbolundefinedobjectfunctionbigint

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 用来检查对象是不是某个类的实例。它适用于自定义类或者内置类(如 DateArray)。

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 加类型断言安全多了。

个人建议:刚开始用 TypeScript 时,可以多写联合类型和类型守卫。它们能让你在写代码时就发现很多潜在问题,而不是等到运行时才报错。说白了,类型系统就是你的「代码保镖」。

好,这一章就到这里。下一章我们会聊更高级的类型操作——泛型。到时候你会发现,类型系统原来可以这么灵活。