第2章:类型系统(The Type System)
一、导读
1.1 本章学习目标
第2章深入 TypeScript 的类型系统——这是 TypeScript 最核心的部分。你将学习联合类型、交叉类型、字面量类型、枚举等高级类型概念,理解 TypeScript 如何通过类型组合来表达复杂的业务逻辑。
本章的核心学习目标包括:
- 掌握联合类型(Union Types):一个值可以是多种类型之一
- 理解交叉类型(Intersection Types):合并多个类型为一个
- 掌握字面量类型(Literal Types):精确到具体值的类型
- 理解枚举类型(Enum Types):命名常量的集合
- 学会使用类型别名(Type Aliases)创建可复用的类型
- 掌握类型收窄(Type Narrowing)的各种方法
1.2 为什么本章如此重要
TypeScript 的类型系统不仅仅是"给变量加个类型标注"那么简单。它的类型系统是一个强大的领域建模工具——通过组合基本类型,你可以精确地描述业务规则,让编译器帮你验证数据的合法性。
比如,一个 HTTP 响应要么是成功(带数据),要么是失败(带错误码)——用联合类型可以精确表达这种"互斥"关系。一个用户配置对象需要同时满足多个接口的约束——用交叉类型可以组合这些约束。一个状态机只有有限的几个状态——用字面量联合类型可以穷举所有可能。
1.3 核心问题引导
联合类型 A | B 和交叉类型 A & B 有什么区别?各自适用于什么场景?
字面量类型 "GET" | "POST" 和 string 有什么区别?为什么前者更安全?
TypeScript 的枚举有什么争议?为什么有些人推荐使用联合类型替代枚举?
类型收窄(narrowing)是如何工作的?typeof、in、自定义类型守卫有什么区别?
如何设计"不可表示非法状态"的类型?
二、核心概念详解
2.1 联合类型(Union Types)
2.1.1 基本概念
联合类型表示一个值可以是多种类型之一:
type StringOrNumber = string | number;
function formatValue(value: StringOrNumber): string {
if (typeof value === "string") {
return value.toUpperCase();
}
return value.toFixed(2);
}
formatValue("hello"); // "HELLO"
formatValue(3.14159); // "3.14"2.1.2 联合类型的收窄
访问联合类型的属性或方法时,只能访问所有成员类型共有的部分:
type Pet = { name: string; type: "dog" } | { name: string; type: "cat"; declawed: boolean };
function processPet(pet: Pet) {
console.log(pet.name); // OK,所有成员都有 name
// console.log(pet.declawed); // 错误!不是所有成员都有 declawed
if (pet.type === "cat") {
console.log(pet.declawed); // OK!TypeScript 收窄了类型
}
}2.2 交叉类型(Intersection Types)
交叉类型将多个类型合并为一个,新类型拥有所有成员类型的属性:
interface HasName {
name: string;
}
interface HasAge {
age: number;
}
type Person = HasName & HasAge;
const alice: Person = {
name: "Alice",
age: 30
};
// 实际应用场景:组合 mixins
type Timestamped = { createdAt: Date; updatedAt: Date };
type SoftDeletable = { deletedAt: Date | null };
type User = { id: string; name: string } & Timestamped & SoftDeletable;2.3 字面量类型(Literal Types)
字面量类型将类型的范围缩小到具体的值:
// 字符串字面量类型
type Direction = "north" | "south" | "east" | "west";
function move(direction: Direction, distance: number) {
// ...
}
move("north", 5); // OK
// move("up", 5); // 错误!"up" 不是 Direction
// 数字字面量类型
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
// 布尔字面量类型
type True = true;
type False = false;2.3.1 判别联合类型(Discriminated Unions)
字面量类型最强大的应用是判别联合类型——使用一个字面量属性作为"判别器":
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; sideLength: number }
| { kind: "triangle"; base: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
case "triangle":
return 0.5 * shape.base * shape.height;
}
}2.4 枚举类型
2.4.1 数字枚举
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
let dir: Direction = Direction.Up;2.4.2 字符串枚举
enum HttpMethod {
GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE"
}
function request(method: HttpMethod, url: string) {
// ...
}
request(HttpMethod.GET, "/api/users");2.4.3 枚举的替代方案
很多 TypeScript 开发者推荐使用联合类型替代枚举:
// 替代方案1:联合类型
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
// 替代方案2:const 对象
const HttpMethod = {
GET: "GET",
POST: "POST",
PUT: "PUT",
DELETE: "DELETE"
} as const;
type HttpMethodType = typeof HttpMethod[keyof typeof HttpMethod];2.5 类型收窄(Type Narrowing)
2.5.1 typeof 收窄
function process(value: string | number | boolean) {
if (typeof value === "string") {
value.toUpperCase(); // value 是 string
} else if (typeof value === "number") {
value.toFixed(2); // value 是 number
} else {
!value; // value 是 boolean
}
}2.5.2 in 收窄
interface Fish { swim(): void }
interface Bird { fly(): void }
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim(); // animal 是 Fish
} else {
animal.fly(); // animal 是 Bird
}
}2.5.3 自定义类型守卫
interface Cat { meow(): void; purr(): void }
interface Dog { bark(): void; fetch(): void }
function isCat(animal: Cat | Dog): animal is Cat {
return "meow" in animal;
}
function makeSound(animal: Cat | Dog) {
if (isCat(animal)) {
animal.meow(); // animal 是 Cat
} else {
animal.bark(); // animal 是 Dog
}
}2.6 类型别名
// 基本类型别名
type ID = string | number;
// 复杂类型别名
type Callback<T> = (error: Error | null, result: T) => void;
// 递归类型别名
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
// 配合泛型
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };三、实战案例
3.1 案例一:类型安全的状态机
type ConnectionState =
| { status: "disconnected" }
| { status: "connecting"; attempt: number }
| { status: "connected"; sessionId: string }
| { status: "error"; message: string; retryable: boolean };
class ConnectionManager {
private state: ConnectionState = { status: "disconnected" };
transition(next: ConnectionState) {
const valid = this.isValidTransition(this.state, next);
if (!valid) {
throw new Error(`Invalid transition: ${this.state.status} -> ${next.status}`);
}
this.state = next;
}
private isValidTransition(from: ConnectionState, to: ConnectionState): boolean {
switch (from.status) {
case "disconnected":
return to.status === "connecting";
case "connecting":
return to.status === "connected" || to.status === "error";
case "connected":
return to.status === "disconnected" || to.status === "error";
case "error":
return to.retryable ? to.status === "connecting" : to.status === "disconnected";
}
}
getStatus(): string {
switch (this.state.status) {
case "disconnected":
return "Disconnected";
case "connecting":
return `Connecting (attempt ${this.state.attempt})`;
case "connected":
return `Connected (session: ${this.state.sessionId})`;
case "error":
return `Error: ${this.state.message}`;
}
}
}3.2 案例二:类型安全的表单验证
type ValidationRule<T> = {
field: keyof T;
validate: (value: T[keyof T]) => string | null;
};
interface LoginForm {
email: string;
password: string;
rememberMe: boolean;
}
const rules: ValidationRule<LoginForm>[] = [
{
field: "email",
validate: (value) => {
if (typeof value !== "string" || !value.includes("@")) {
return "Invalid email address";
}
return null;
}
},
{
field: "password",
validate: (value) => {
if (typeof value !== "string" || value.length < 8) {
return "Password must be at least 8 characters";
}
return null;
}
}
];
function validate<T extends object>(form: T, rules: ValidationRule<T>[]): Record<keyof T, string | null> {
const errors = {} as Record<keyof T, string | null>;
for (const key of Object.keys(form) as (keyof T)[]) {
errors[key] = null;
}
for (const rule of rules) {
const error = rule.validate(form[rule.field]);
if (error) {
errors[rule.field] = error;
}
}
return errors;
}3.3 案例三:类型安全的 API 响应处理
type ApiResponse<T> =
| { status: 200; data: T }
| { status: 400; error: string }
| { status: 401; error: string; redirectUrl: string }
| { status: 404; error: string }
| { status: 500; error: string; traceId: string };
interface User {
id: string;
name: string;
email: string;
}
async function fetchUser(id: string): Promise<User | null> {
const response: ApiResponse<User> = await apiCall(`/users/${id}`);
switch (response.status) {
case 200:
return response.data;
case 401:
window.location.href = response.redirectUrl;
return null;
case 404:
console.warn(`User ${id} not found`);
return null;
case 400:
throw new Error(`Bad request: ${response.error}`);
case 500:
throw new Error(`Server error: ${response.error} (trace: ${response.traceId})`);
}
}四、本章小结
第2章深入讲解了 TypeScript 的类型系统:
联合类型:A | B 表示值可以是 A 或 B,通过类型收窄访问特定成员。
交叉类型:A & B 合并多个类型的属性。
字面量类型:将类型范围缩小到具体值,是判别联合类型的基础。
枚举:命名常量集合,但联合类型通常是更好的替代方案。
类型收窄:typeof、in、自定义类型守卫让编译器理解类型的具体形态。
五、思考与练习
思考题
联合类型和交叉类型在集合论中分别对应什么操作?
为什么判别联合类型(Discriminated Unions)如此重要?它如何帮助实现"不可表示非法状态"?
TypeScript 的枚举有什么缺陷?为什么社区倾向于使用联合类型替代?
如何设计一个类型系统,使得"不可能的状态不可表示"?
编程练习
使用判别联合类型实现一个购物车状态:空购物车、有商品的购物车、已结账的购物车。
编写一个类型安全的 EventEmitter,确保事件名和回调参数类型匹配。
实现一个类型安全的 Result<T, E> 类型,包含 Ok 和 Err 两个变体。