TypeScript 开发指南 / 08 - 联合与交叉类型
联合与交叉类型
联合类型(Union Types)
联合类型表示一个值可以是多种类型之一,使用 | 分隔:
// 基本联合类型
let value: string | number;
value = "hello"; // ✅
value = 42; // ✅
value = true; // ❌ boolean 不在联合类型中
// 函数参数联合类型
function printId(id: string | number): void {
console.log(`ID: ${id}`);
}
printId("abc"); // ✅
printId(123); // ✅
联合类型的限制
function getLength(value: string | number): number {
// ❌ 不能直接调用只属于某个类型的方法
// return value.length; // number 没有 length
// ✅ 需要先进行类型收窄
if (typeof value === "string") {
return value.length; // 这里 value 被收窄为 string
}
return value.toString().length;
}
字面量类型(Literal Types)
字面量类型是更具体的类型,限定变量只能是某个特定的值:
// 字符串字面量类型
type Direction = "up" | "down" | "left" | "right";
let dir: Direction = "up"; // ✅
dir = "forward"; // ❌ 不在字面量类型中
// 数字字面量类型
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
let roll: DiceRoll = 3; // ✅
roll = 7; // ❌
// 布尔字面量类型
type True = true;
let flag: True = true; // ✅
flag = false; // ❌
模板字面量类型(Template Literal Types)
// 基本模板字面量
type Greeting = `Hello, ${string}!`;
const msg1: Greeting = "Hello, World!"; // ✅
const msg2: Greeting = "Hi, World!"; // ❌
// 联合展开
type Color = "red" | "blue" | "green";
type Size = "small" | "medium" | "large";
type ColorSize = `${Color}-${Size}`;
// "red-small" | "red-medium" | "red-large" | "blue-small" | ...
// 内置字符串操作
type UpperDirection = Uppercase<Direction>; // "UP" | "DOWN" | ...
type LowerDirection = Lowercase<Direction>; // "up" | "down" | ...
type CapDirection = Capitalize<Direction>; // "Up" | "Down" | ...
as const 断言
// 普通声明会被推断为宽泛类型
const colors = ["red", "green", "blue"]; // string[]
// as const 使所有内容变为只读字面量类型
const COLORS = ["red", "green", "blue"] as const;
// 类型: readonly ["red", "green", "blue"]
// 用于对象
const config = {
host: "localhost",
port: 3000,
debug: false
} as const;
// 类型: { readonly host: "localhost"; readonly port: 3000; readonly debug: false }
// 配合 typeof 获取字面量类型
type Config = typeof config;
// { readonly host: "localhost"; readonly port: 3000; readonly debug: false }
交叉类型(Intersection Types)
交叉类型将多个类型合并为一个类型,使用 & 分隔:
// 基本交叉类型
type Named = { name: string };
type Aged = { age: number };
type Employed = { company: string };
// 交叉类型:同时满足所有类型
type Person = Named & Aged & Employed;
const person: Person = {
name: "Alice",
age: 25,
company: "Tech Corp"
};
// 三个属性都必须有,缺一不可
交叉类型 vs 接口继承
// 接口继承
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// 交叉类型(等价)
type AnimalType = { name: string };
type DogType = AnimalType & { breed: string };
// 两者效果相同,选择依据:
// - 接口继承:更清晰,支持声明合并
// - 交叉类型:更灵活,可用于联合类型组合
联合与交叉的组合
// 联合类型的交叉
type Result = (string | number) & (boolean | string);
// 等价于 string(两种类型都包含 string)
// 对象联合类型
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
// 判别联合(Discriminated Union)
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return 0.5 * shape.base * shape.height;
}
}
可辨识联合(Discriminated Unions)
这是 TypeScript 中最实用的模式之一:
// 每个成员都有一个共同的可辨识属性(discriminant)
type ApiResponse<T> =
| { status: "success"; data: T }
| { status: "error"; error: string }
| { status: "loading" };
function handleResponse<T>(response: ApiResponse<T>): void {
switch (response.status) {
case "success":
console.log(response.data); // ✅ T 类型
break;
case "error":
console.log(response.error); // ✅ string 类型
break;
case "loading":
console.log("加载中...");
break;
}
}
复杂可辨识联合
// 事件系统
type Event =
| { type: "click"; x: number; y: number }
| { type: "keypress"; key: string; code: string }
| { type: "scroll"; deltaX: number; deltaY: number }
| { type: "resize"; width: number; height: number };
function handleEvent(event: Event): void {
switch (event.type) {
case "click":
console.log(`点击位置: (${event.x}, ${event.y})`);
break;
case "keypress":
console.log(`按键: ${event.key} (${event.code})`);
break;
case "scroll":
console.log(`滚动: (${event.deltaX}, ${event.deltaY})`);
break;
case "resize":
console.log(`尺寸: ${event.width}x${event.height}`);
break;
}
}
never 类型与穷尽检查
type Shape = "circle" | "square" | "triangle";
function getArea(shape: Shape): number {
switch (shape) {
case "circle":
return Math.PI * 100;
case "square":
return 100;
case "triangle":
return 50;
default:
// 穷尽检查:如果遗漏了某个 case,这里会报错
const _exhaustive: never = shape;
return _exhaustive;
}
}
// 如果后来添加了新类型 "pentagon",default 分支会报错:
// Type '"pentagon"' is not assignable to type 'never'
类型缩窄与守卫
// typeof 类型守卫
function process(value: string | number | boolean): string {
if (typeof value === "string") {
return value.toUpperCase(); // value: string
}
if (typeof value === "number") {
return value.toFixed(2); // value: number
}
return value.toString(); // value: boolean
}
// instanceof 类型守卫
function formatDate(input: string | Date): string {
if (input instanceof Date) {
return input.toISOString(); // input: Date
}
return new Date(input).toISOString(); // input: string
}
// in 类型守卫
interface Bird { fly(): void; layEggs(): void; }
interface Fish { swim(): void; layEggs(): void; }
function move(animal: Bird | Fish): void {
if ("fly" in animal) {
animal.fly(); // animal: Bird
} else {
animal.swim(); // animal: Fish
}
}
自定义类型守卫(Type Predicates)
// 返回类型是 pet is Fish(类型谓词)
function isFish(pet: Bird | Fish): pet is Fish {
return "swim" in pet;
}
// 使用
const pet: Bird | Fish = getPet();
if (isFish(pet)) {
pet.swim(); // ✅ pet: Fish
} else {
pet.fly(); // ✅ pet: Bird
}
// 更多示例
function isString(value: unknown): value is string {
return typeof value === "string";
}
function isNotNull<T>(value: T | null): value is T {
return value !== null;
}
// 数组过滤
const values: (string | null)[] = ["hello", null, "world", null];
const strings = values.filter(isNotNull); // string[]
断言函数(Assertion Functions)
// 断言函数:如果不满足条件则抛出错误
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error(`Expected string, got ${typeof value}`);
}
}
function process(value: unknown): void {
assertIsString(value);
// 这里 value 已经被断言为 string
console.log(value.toUpperCase()); // ✅
}
// assertNever 用于穷尽检查
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
type Status = "active" | "inactive";
function handleStatus(status: Status): string {
switch (status) {
case "active": return "活跃";
case "inactive": return "未激活";
default: return assertNever(status);
}
}
业务场景:表单状态管理
// 使用可辨识联合描述表单状态
type FormState<T> =
| { status: "idle" }
| { status: "submitting" }
| { status: "success"; data: T }
| { status: "error"; errors: Record<string, string[]> };
interface User {
id: number;
name: string;
email: string;
}
function renderFormState(state: FormState<User>): string {
switch (state.status) {
case "idle":
return "请填写表单";
case "submitting":
return "提交中...";
case "success":
return `欢迎,${state.data.name}!`;
case "error":
const messages = Object.values(state.errors).flat();
return `错误:${messages.join(", ")}`;
}
}
// 使用
const state: FormState<User> = {
status: "success",
data: { id: 1, name: "Alice", email: "[email protected]" }
};
console.log(renderFormState(state));
注意事项
- 联合类型只能访问公共属性——需要类型收窄后才能访问特定类型的属性
- 可辨识联合是处理复杂联合类型的最佳实践
- as const 可以将值变为字面量类型,常与联合类型配合使用
- never 类型用于穷尽检查,确保处理了所有可能的情况
- 自定义类型守卫返回
value is Type,用于在条件分支中收窄类型