1. 联合类型:TypeScript中的灵活武器
在TypeScript的世界里,联合类型(Union Types)就像一把瑞士军刀,它能让你用一行代码表达"这个变量可以是A类型或B类型"的语义。想象你正在开发一个电商系统,商品价格可能显示为数字(如29.9)或字符串(如"促销中")。传统的静态类型语言会让你陷入两难,而TypeScript的联合类型number | string就能优雅解决这个问题。
联合类型的语法简单直接:用竖线|连接多个类型。比如let id: string | number表示id可以是字符串或数字。这种灵活性不是类型安全的妥协,而是TypeScript类型系统的精妙设计——它会在编译时检查你对联合类型变量的所有操作是否对每个成员类型都有效。
提示:虽然JavaScript本身是动态类型的,但TypeScript通过联合类型等特性,在保持灵活性的同时提供了强大的类型安全保障。这就像给你的代码加了个智能保镖,既不会限制你的行动自由,又能防止你做出危险动作。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 联合类型的定义与语法深度解析
2.1 基础定义方式
联合类型的核心语法是T1 | T2 | ... | Tn,其中T代表任意类型。这个语法糖背后是TypeScript的类型推断系统在起作用。当你声明let value: string | number时,编译器会创建一个类型保护边界,确保所有对value的操作都同时兼容string和number。
typescript复制// 基本联合类型示例
type ID = string | number;
let userId: ID = "abc123"; // 合法
userId = 123; // 也合法
userId = true; // 报错:boolean不在联合类型中
2.2 复杂类型联合
联合类型真正的威力在于可以组合任意复杂类型。你可以联合接口、泛型、甚至其他联合类型:
typescript复制interface Car {
brand: string;
speed: number;
}
interface Bike {
type: string;
pedal: boolean;
}
type Vehicle = Car | Bike;
const myVehicle: Vehicle = {
brand: 'Tesla',
speed: 250
}; // 合法Car类型
const yourVehicle: Vehicle = {
type: 'Mountain',
pedal: true
}; // 合法Bike类型
2.3 字面量联合类型
一种特别有用的模式是字面量联合类型,它把值直接作为类型使用:
typescript复制type Direction = 'up' | 'down' | 'left' | 'right';
function move(dir: Direction) {
// ...
}
move('up'); // 合法
move('north'); // 报错
这种模式在Redux的action类型、配置选项等场景非常实用,它能确保你永远不会拼错关键字符串。
3. 联合类型的六大核心使用场景
3.1 处理不确定的API响应
在实际开发中,API返回的数据结构往往存在变数。联合类型能优雅地处理这种不确定性:
typescript复制type APIResponse = {
status: 'success',
data: User[]
} | {
status: 'error',
message: string
};
function handleResponse(response: APIResponse) {
if (response.status === 'success') {
console.log(response.data); // 这里能安全访问data
} else {
console.error(response.message); // 这里能安全访问message
}
}
这种模式被称为"可辨识联合"(Discriminated Unions),通过一个共同的字段(这里是status)来区分不同类型。
3.2 函数参数的多态处理
当函数需要接受多种类型的参数时,联合类型比any更安全:
typescript复制function formatInput(input: string | number) {
if (typeof input === 'string') {
return input.trim().toUpperCase();
}
return input.toFixed(2);
}
TypeScript的类型收窄(Type Narrowing)机制会根据typeof检查自动调整类型上下文。
3.3 组件Props的灵活定义
在前端框架如React中,联合类型能让组件Props更灵活:
typescript复制type ButtonProps = {
size: 'small' | 'medium' | 'large';
theme: 'primary' | 'secondary' | 'danger';
onClick?: () => void;
} & (
{ type: 'button'; text: string } |
{ type: 'icon'; icon: JSX.Element }
);
function Button(props: ButtonProps) {
// 实现会根据type自动区分text或icon
}
3.4 状态管理的类型安全
在状态管理中,联合类型可以完美表达各种可能的状态:
typescript复制type AuthState =
{ status: 'idle' } |
{ status: 'loading', requestId: string } |
{ status: 'success', user: User } |
{ status: 'error', error: Error };
3.5 兼容历史代码
当逐步迁移JavaScript代码到TypeScript时,联合类型能平滑处理尚未完全类型化的部分:
typescript复制type LegacyData = string | { [key: string]: any } | any[];
function processLegacyData(data: LegacyData) {
// 渐进式类型化处理
}
3.6 配置对象的可选模式
对于有多种配置模式的工具函数,联合类型比可选参数更清晰:
typescript复制type LoggerConfig =
{ level: 'debug', debugFile: string } |
{ level: 'error', emailAlerts: boolean };
function setupLogger(config: LoggerConfig) {
// 根据level自动推断可用属性
}
4. 联合类型的进阶实践技巧
4.1 类型守卫与类型谓词
当联合类型变得复杂时,需要更强大的类型收窄手段:
typescript复制interface Cat {
meow(): void;
}
interface Dog {
bark(): void;
}
function isCat(animal: Cat | Dog): animal is Cat {
return (animal as Cat).meow !== undefined;
}
function handleAnimal(animal: Cat | Dog) {
if (isCat(animal)) {
animal.meow(); // 安全调用
} else {
animal.bark();
}
}
4.2 穷尽性检查
利用never类型确保处理了联合类型的所有情况:
typescript复制type Shape =
{ kind: 'circle'; radius: number } |
{ kind: 'square'; size: 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.size ** 2;
case 'triangle': return 0.5 * shape.base * shape.height;
default:
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
如果新增了Shape类型但忘记更新getArea,default分支会报错。
4.3 联合类型与泛型结合
泛型可以增强联合类型的表达能力:
typescript复制type Result<T> =
{ success: true; value: T } |
{ success: false; error: string };
function safeParseJSON<T>(json: string): Result<T> {
try {
return { success: true, value: JSON.parse(json) };
} catch (e) {
return { success: false, error: e.message };
}
}
4.4 递归联合类型
联合类型可以用于定义递归数据结构:
typescript复制type JSONValue =
| string
| number
| boolean
| null
| { [key: string]: JSONValue }
| JSONValue[];
const jsonData: JSONValue = {
name: "John",
age: 30,
scores: [90, 85, 95],
metadata: {
active: true,
tags: ["vip", "early-adopter"]
}
};
5. 常见陷阱与最佳实践
5.1 过度使用联合类型
虽然联合类型强大,但滥用会导致类型系统复杂化。当联合超过3-4种类型时,考虑:
- 是否应该拆分成多个函数/接口?
- 是否可以用继承或组合替代?
- 是否某些类型可以合并?
5.2 忽略类型收窄
错误示例:
typescript复制function printId(id: string | number) {
console.log(id.toUpperCase()); // 报错:number没有toUpperCase
}
正确做法:
typescript复制function printId(id: string | number) {
if (typeof id === 'string') {
console.log(id.toUpperCase());
} else {
console.log(id.toString());
}
}
5.3 与交叉类型混淆
联合类型A | B表示"A或B",而交叉类型A & B表示"同时是A和B"。一个常见错误是混淆两者:
typescript复制// 错误理解
type Confused = { x: number } | { y: string };
const obj: Confused = { x: 1, y: 'hi' }; // 实际上这是合法的,因为符合{ x: number }部分
// 正确区分
type Union = { x: number } | { y: string };
type Intersection = { x: number } & { y: string };
5.4 性能考量
深层嵌套的大型联合类型可能会影响编译器性能。如果遇到编译变慢,可以考虑:
- 将复杂联合类型拆分为多个类型别名
- 减少递归深度
- 避免在热路径代码中使用过于复杂的联合类型
6. 联合类型与其他TypeScript特性的协作
6.1 与类型别名和接口的配合
联合类型可以与类型别名(type)和接口(interface)无缝协作:
typescript复制interface Admin {
role: 'admin';
permissions: string[];
}
interface User {
role: 'user';
lastLogin: Date;
}
type Person = Admin | User;
function greet(person: Person) {
if (person.role === 'admin') {
console.log(`Admin with ${person.permissions.length} permissions`);
} else {
console.log(`User last logged in at ${person.lastLogin}`);
}
}
6.2 与泛型约束的结合
通过泛型约束可以创建更灵活的联合类型:
typescript复制type Primitive = string | number | boolean | symbol;
function logValue<T extends Primitive>(value: T): T {
console.log(value);
return value;
}
logValue('hello'); // 合法
logValue(42); // 合法
logValue({}); // 报错
6.3 在条件类型中的应用
联合类型在条件类型中表现出色:
typescript复制type ExtractString<T> = T extends string ? T : never;
type StringValues = ExtractString<'a' | 1 | true | 'b'>; // 结果为 'a' | 'b'
6.4 与映射类型的互动
联合类型可以驱动映射类型的行为:
typescript复制type EventMap = {
click: MouseEvent;
scroll: UIEvent;
keypress: KeyboardEvent;
};
type EventHandlers = {
[K in keyof EventMap]?: (event: EventMap[K]) => void;
};
type EventNames = keyof EventMap; // "click" | "scroll" | "keypress"
7. 真实项目中的联合类型案例
7.1 Redux Action模式
Redux中经典的Action定义就是联合类型的绝佳用例:
typescript复制type Action =
| { type: 'ADD_TODO'; text: string }
| { type: 'TOGGLE_TODO'; id: number }
| { type: 'DELETE_TODO'; id: number }
| { type: 'SET_VISIBILITY_FILTER'; filter: string };
function todosReducer(state: Todo[], action: Action): Todo[] {
switch (action.type) {
case 'ADD_TODO':
// 这里action.text可用
return [...state, newTodo(action.text)];
case 'TOGGLE_TODO':
// 这里action.id可用
return state.map(todo =>
todo.id === action.id ? {...todo, done: !todo.done} : todo
);
// 其他case处理...
}
}
7.2 API响应处理
处理GraphQL等API的响应时,联合类型能优雅处理嵌套数据:
typescript复制type APIResponse<T> =
| { status: 200; data: T }
| { status: 400; error: string }
| { status: 500; error: string; stack?: string };
async function fetchUser(id: string): Promise<APIResponse<User>> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return { status: response.status, error: await response.text() };
}
return { status: 200, data: await response.json() };
} catch (error) {
return { status: 500, error: error.message, stack: error.stack };
}
}
7.3 表单验证系统
构建类型安全的表单验证器:
typescript复制type ValidationResult =
| { valid: true; value: string }
| { valid: false; error: string; field: string };
function validateEmail(email: string): ValidationResult {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (re.test(email)) {
return { valid: true, value: email };
}
return { valid: false, error: 'Invalid email format', field: 'email' };
}
7.4 路由系统类型定义
在单页应用路由系统中:
typescript复制type Route =
| { path: '/'; component: Home }
| { path: '/about'; component: About }
| { path: '/users/:id'; component: UserDetail; params: { id: string } }
| { path: '/search'; component: Search; query: { q?: string } };
function matchRoute(path: string): Route | null {
// 路由匹配逻辑...
}
8. 调试联合类型的技巧
8.1 类型打印技巧
当联合类型复杂时,可以用工具类型查看:
typescript复制type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
type SomeComplexUnion = { a: number } | { b: string };
type Expanded = Expand<SomeComplexUnion>;
/*
在IDE中悬停查看Expanded会显示:
{
a: number;
b?: undefined;
} | {
b: string;
a?: undefined;
}
*/
8.2 错误信息解读
当看到错误"Property 'x' does not exist on type 'A | B'"时,说明你需要类型收窄:
typescript复制type A = { x: number };
type B = { y: string };
function doSomething(obj: A | B) {
console.log(obj.x); // 错误:x可能不存在于B上
// 解决方案1:类型断言(不推荐)
console.log((obj as A).x);
// 解决方案2:类型守卫(推荐)
if ('x' in obj) {
console.log(obj.x); // 安全
}
}
8.3 测试联合类型
编写测试时确保覆盖所有联合分支:
typescript复制type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
function testResult() {
const success: Result<number, string> = { ok: true, value: 42 };
const failure: Result<number, string> = { ok: false, error: "Oops" };
// 测试成功路径
if (success.ok) {
assert.equal(success.value, 42);
}
// 测试失败路径
if (!failure.ok) {
assert.equal(failure.error, "Oops");
}
}
8.4 性能优化提示
当遇到大型联合类型导致的编译性能问题时:
- 使用
type而非interface定义联合成员(interface会参与声明合并) - 避免深度嵌套的联合类型
- 考虑将大型联合拆分为多个小联合
- 在tsconfig.json中启用
"skipLibCheck": true
