TypeScript 联合类型详解
联合类型是 TypeScript 中一个强大的类型系统特性,它允许一个值可以是多种类型中的一种。联合类型提供了类型安全的灵活性,是处理不确定类型场景的重要工具。
1. 联合类型概述
1.1 什么是联合类型?
联合类型表示一个值可以是多种类型中的一种,使用竖线 | 分隔各个类型。
| // 变量可以是 string 或 number 类型
let value: string | number;
value = "Hello"; // 正确:string 类型
value = 42; // 正确:number 类型
value = true; // 错误:boolean 类型不在联合类型中
|
1.2 联合类型的优势
- 类型安全:限制变量只能是指定类型之一
- 灵活性:处理多种可能的输入类型
- 可读性:明确表达变量的可能类型
- 重构友好:类型系统帮助发现潜在问题
2. 基本用法
2.1 变量声明
| // 基本类型联合
let id: string | number;
id = "user-123"; // OK
id = 123; // OK
// id = true; // Error: Type 'boolean' is not assignable to type 'string | number'
// 多个类型联合
let status: "success" | "error" | "loading";
status = "success"; // OK
status = "error"; // OK
// status = "failed"; // Error: Type '"failed"' is not assignable
// 对象类型联合
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
type Shape = Circle | Square;
let shape: Shape = { kind: "circle", radius: 5 }; // OK
|
2.2 函数参数
| // 函数接受联合类型参数
function formatValue(value: string | number): string {
return `Value: ${value}`;
}
formatValue("Hello"); // "Value: Hello"
formatValue(42); // "Value: 42"
// formatValue(true); // Error: Argument of type 'boolean' is not assignable
// 返回联合类型
function parseInput(input: string): number | null {
const num = Number(input);
return isNaN(num) ? null : num;
}
const result1 = parseInput("42"); // number | null
const result2 = parseInput("hello"); // number | null
|
2.3 数组中的联合类型
| // 数组元素可以是多种类型
let mixedArray: (string | number)[] = ["Hello", 42, "World", 100];
// 只读数组
const readonlyMixed: readonly (string | number)[] = ["a", 1, "b", 2];
// 元组中的联合类型
let tuple: [string, number | boolean, string?];
tuple = ["id", 123, "optional"]; // OK
tuple = ["id", true]; // OK (第三个元素可选)
// tuple = [123, "id"]; // Error: 位置不匹配
|
3. 类型守卫与类型收窄
3.1 typeof 类型守卫
| function processValue(value: string | number) {
// 使用 typeof 收窄类型
if (typeof value === "string") {
// 这里 value 的类型被收窄为 string
console.log(value.toUpperCase());
} else {
// 这里 value 的类型被收窄为 number
console.log(value.toFixed(2));
}
}
// 更复杂的例子
function format(input: string | number | boolean): string {
if (typeof input === "string") {
return `String: ${input}`;
} else if (typeof input === "number") {
return `Number: ${input.toFixed(2)}`;
} else {
return `Boolean: ${input}`;
}
}
|
3.2 instanceof 类型守卫
| class Dog {
bark() {
console.log("Woof!");
}
}
class Cat {
meow() {
console.log("Meow!");
}
}
function handlePet(pet: Dog | Cat) {
if (pet instanceof Dog) {
pet.bark(); // OK: pet 被收窄为 Dog
} else {
pet.meow(); // OK: pet 被收窄为 Cat
}
}
const myDog = new Dog();
const myCat = new Cat();
handlePet(myDog); // "Woof!"
handlePet(myCat); // "Meow!"
|
3.3 自定义类型守卫
| // 用户定义的类型守卫
function isString(value: any): value is string {
return typeof value === "string";
}
function isNumber(value: any): value is number {
return typeof value === "number" && !isNaN(value);
}
function process(input: string | number) {
if (isString(input)) {
console.log(`String length: ${input.length}`);
} else if (isNumber(input)) {
console.log(`Number squared: ${input * input}`);
}
}
// 更复杂的自定义类型守卫
interface User {
id: number;
name: string;
email: string;
}
interface Admin {
id: number;
name: string;
permissions: string[];
}
function isAdmin(user: User | Admin): user is Admin {
return "permissions" in user;
}
function getUserInfo(user: User | Admin) {
if (isAdmin(user)) {
console.log(`Admin permissions: ${user.permissions.join(", ")}`);
} else {
console.log(`User email: ${user.email}`);
}
}
|
3.4 判别式联合
| // 使用字面量类型作为判别属性
type Circle = {
kind: "circle";
radius: number;
};
type Square = {
kind: "square";
side: number;
};
type Rectangle = {
kind: "rectangle";
width: number;
height: number;
};
type Shape = Circle | Square | Rectangle;
function getArea(shape: Shape): number {
// 使用 switch 语句进行类型收窄
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
case "rectangle":
return shape.width * shape.height;
default:
// 确保处理了所有情况
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
// 使用示例
const circle: Circle = { kind: "circle", radius: 5 };
const square: Square = { kind: "square", side: 4 };
const rectangle: Rectangle = { kind: "rectangle", width: 3, height: 6 };
console.log(getArea(circle)); // 78.53981633974483
console.log(getArea(square)); // 16
console.log(getArea(rectangle)); // 18
|
4. 联合类型的高级用法
4.1 与交叉类型结合
| // 联合类型与交叉类型
type StringOrNumber = string | number;
type BooleanOrNull = boolean | null;
// 交叉类型:必须同时满足所有类型
type ComplexType = (StringOrNumber) & { metadata: any };
// 实际使用
const obj1: ComplexType = {
metadata: { created: new Date() }
}; // Error: 缺少 StringOrNumber 类型
const obj2: ComplexType = {
value: "hello",
metadata: { created: new Date() }
} as any; // 需要类型断言
// 更实用的例子
type WithId = { id: string | number };
type WithName = { name: string };
type WithEmail = { email: string };
type User = WithId & (WithName | WithEmail);
const user1: User = { id: 1, name: "Alice" }; // OK
const user2: User = { id: "user-2", email: "bob@test.com" }; // OK
// const user3: User = { id: 3 }; // Error: 缺少 name 或 email
|
4.2 条件类型中的联合类型
| // 条件类型
type ExtractString<T> = T extends string ? T : never;
type Result1 = ExtractString<string | number | boolean>; // string
type Result2 = ExtractString<number | boolean>; // never
// 分布式条件类型
type ToArray<T> = T extends any ? T[] : never;
type StringArray = ToArray<string>; // string[]
type UnionArray = ToArray<string | number>; // string[] | number[]
// 非分布式版本
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type NonDistArray = ToArrayNonDist<string | number>; // (string | number)[]
|
4.3 映射类型中的联合类型
| // 使用联合类型作为键
type UserRoles = "admin" | "editor" | "viewer";
type Permissions = {
[K in UserRoles]: boolean;
};
const permissions: Permissions = {
admin: true,
editor: true,
viewer: false
};
// 更复杂的映射
type EventTypes = "click" | "hover" | "focus";
type EventHandlers = {
[K in EventTypes]: (event: Event) => void;
};
const handlers: EventHandlers = {
click: (e) => console.log("clicked"),
hover: (e) => console.log("hovered"),
focus: (e) => console.log("focused")
};
|
5. 实用工具类型
5.1 内置工具类型
| // Extract: 从联合类型中提取指定类型
type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
type T1 = Extract<string | number | (() => void), Function>; // () => void
// Exclude: 从联合类型中排除指定类型
type T2 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T3 = Exclude<string | number | (() => void), Function>; // string | number
// NonNullable: 排除 null 和 undefined
type T4 = NonNullable<string | number | null | undefined>; // string | number
// 实际应用
type FormField = {
type: "text" | "number" | "checkbox";
value: string | number | boolean;
required?: boolean;
};
// 提取特定类型的字段
type TextField = Extract<FormField, { type: "text" }>;
type NumberField = Extract<FormField, { type: "number" }>;
// 排除特定类型的字段
type NonTextFields = Exclude<FormField, { type: "text" }>;
|
5.2 自定义工具类型
| // 获取联合类型的所有可能值
type ValueOf<T> = T[keyof T];
interface Config {
api: {
endpoint: string;
timeout: number;
};
ui: {
theme: "light" | "dark";
language: string;
};
}
type ConfigValues = ValueOf<Config>;
// 等价于 { endpoint: string; timeout: number } | { theme: "light" | "dark"; language: string }
// 联合类型转元组
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type UnionToTuple<T> = UnionToIntersection<
T extends any ? (t: T) => T : never
> extends (_: any) => infer W
? [...UnionToTuple<Exclude<T, W>>, W]
: [];
type Test = UnionToTuple<"a" | "b" | "c">; // ["a", "b", "c"]
|
6. 实际应用场景
6.1 API 响应处理
| // API 响应类型
type ApiResponse<T> = {
success: true;
data: T;
timestamp: Date;
} | {
success: false;
error: string;
code: number;
timestamp: Date;
};
// 处理函数
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
try {
const response = await fetch(url);
if (!response.ok) {
return {
success: false,
error: `HTTP ${response.status}`,
code: response.status,
timestamp: new Date()
};
}
const data = await response.json();
return {
success: true,
data,
timestamp: new Date()
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
code: 500,
timestamp: new Date()
};
}
}
// 使用示例
type UserData = { id: number; name: string; email: string };
async function getUser() {
const response = await fetchData<UserData>("/api/user/1");
if (response.success) {
// 类型收窄:response 现在是成功类型
console.log(`User: ${response.data.name}`);
console.log(`Email: ${response.data.email}`);
} else {
// 类型收窄:response 现在是错误类型
console.error(`Error ${response.code}: ${response.error}`);
}
return response;
}
|
6.2 表单验证
| // 验证结果类型
type ValidationResult =
| { valid: true; value: string }
| { valid: false; error: string; field: string };
// 验证函数
function validateEmail(email: string): ValidationResult {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email) {
return { valid: false, error: "Email is required", field: "email" };
}
if (!emailRegex.test(email)) {
return { valid: false, error: "Invalid email format", field: "email" };
}
return { valid: true, value: email };
}
function validatePassword(password: string): ValidationResult {
if (!password) {
return { valid: false, error: "Password is required", field: "password" };
}
if (password.length < 8) {
return { valid: false, error: "Password must be at least 8 characters", field: "password" };
}
return { valid: true, value: password };
}
// 表单处理
function handleSubmit(email: string, password: string) {
const emailResult = validateEmail(email);
const passwordResult = validatePassword(password);
// 检查所有验证结果
const results = [emailResult, passwordResult];
const errors = results.filter((r): r is Extract<ValidationResult, { valid: false }> => !r.valid);
if (errors.length > 0) {
console.error("Validation errors:", errors);
return;
}
// 所有验证通过,可以安全地访问 value
const validResults = results as Extract<ValidationResult, { valid: true }>[];
console.log("Form submitted successfully");
console.log("Email:", validResults[0].value);
console.log("Password:", validResults[1].value);
}
|
6.3 状态管理
| // 应用状态类型
type AppState =
| { status: "idle" }
| { status: "loading"; requestId: string }
| { status: "success"; data: any; timestamp: Date }
| { status: "error"; error: string; retryCount: number };
// 状态转换函数
function handleStateTransition(current: AppState, action: any): AppState {
switch (current.status) {
case "idle":
if (action.type === "FETCH_REQUEST") {
return {
status: "loading",
requestId: action.requestId
};
}
return current;
case "loading":
if (action.type === "FETCH_SUCCESS") {
return {
status: "success",
data: action.data,
timestamp: new Date()
};
}
if (action.type === "FETCH_ERROR") {
return {
status: "error",
error: action.error,
retryCount: 0
};
}
return current;
case "success":
if (action.type === "RESET") {
return { status: "idle" };
}
return current;
case "error":
if (action.type === "RETRY") {
return {
status: "loading",
requestId: `retry-${current.retryCount + 1}`
};
}
return current;
default:
// 确保处理了所有状态
const exhaustiveCheck: never = current;
return exhaustiveCheck;
}
}
// 使用示例
let state: AppState = { status: "idle" };
// 状态转换
state = handleStateTransition(state, { type: "FETCH_REQUEST", requestId: "req-1" });
console.log(state); // { status: "loading", requestId: "req-1" }
state = handleStateTransition(state, { type: "FETCH_SUCCESS", data: { user: "Alice" } });
console.log(state); // { status: "success", data: { user: "Alice" }, timestamp: Date }
|
7. 最佳实践与注意事项
7.1 避免过度使用联合类型
| // 不好的实践:过度使用联合类型
type OverusedUnion = string | number | boolean | null | undefined | object | any[];
// 好的实践:使用更具体的类型
type ValidInput = string | number;
type Maybe<T> = T | null | undefined;
type ApiResponse<T> = { success: true; data: T } | { success: false; error: string };
|
7.2 优先使用判别式联合
| // 不好的实践:没有判别属性
type Shape =
| { radius: number }
| { side: number }
| { width: number; height: number };
// 好的实践:使用判别属性
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "rectangle"; width: number; height: number };
|
7.3 合理使用类型守卫
| // 不好的实践:过度使用类型断言
function process(value: string | number) {
if ((value as any).toUpperCase) {
console.log((value as string).toUpperCase());
}
}
// 好的实践:使用类型守卫
function process(value: string | number) {
if (typeof value === "string") {
console.log(value.toUpperCase()); // 自动类型收窄
}
}
|
7.4 考虑性能影响
| // 注意:大型联合类型可能影响编译性能
// 如果遇到性能问题,考虑:
// 1. 拆分大型联合类型
// 2. 使用接口继承
// 3. 启用增量编译
|
8. 常见错误与解决方案
8.1 无法访问公共属性
| type Circle = { radius: number; color: string };
type Square = { side: number; color: string };
function getColor(shape: Circle | Square) {
// 错误:Property 'color' does not exist on type 'Circle | Square'
// return shape.color;
// 解决方案:使用类型守卫
if ("radius" in shape) {
return shape.color; // OK: shape 被收窄为 Circle
} else {
return shape.color; // OK: shape 被收窄为 Square
}
}
|
8.2 函数重载与联合类型
| // 使用函数重载处理联合类型
function process(input: string): string;
function process(input: number): number;
function process(input: string | number): string | number {
if (typeof input === "string") {
return input.toUpperCase();
} else {
return input * 2;
}
}
// 使用泛型约束
function process<T extends string | number>(input: T): T {
// 需要类型守卫
if (typeof input === "string") {
return input.toUpperCase() as T;
} else {
return (input * 2) as T;
}
}
|
8.3 数组过滤问题
| const mixed: (string | number)[] = ["a", 1, "b", 2];
// 错误:filter 不会改变类型
const strings = mixed.filter(x => typeof x === "string");
// strings 的类型仍然是 (string | number)[]
// 解决方案:使用类型谓词
function isString(x: any): x is string {
return typeof x === "string";
}
const strings2 = mixed.filter(isString);
// strings2 的类型是 string[]
|
9. 总结
联合类型是 TypeScript 类型系统的核心特性之一,它提供了处理多种可能类型的类型安全方式。通过合理使用联合类型,可以:
9.1 主要优势
- 类型安全:确保值只能是指定类型之一
- 表达力强:清晰表达变量的可能类型
- 灵活性高:适应多种业务场景
- 工具友好:IDE 提供更好的智能提示
9.2 关键概念
- 类型收窄:使用类型守卫缩小联合类型的范围
- 判别式联合:使用字面量类型区分联合类型成员
- 工具类型:利用
Extract、Exclude 等工具类型操作联合类型 - 条件类型:在类型级别操作联合类型
9.3 学习建议
- 从简单开始:先掌握基本用法,再学习高级特性
- 实践为主:在实际项目中应用联合类型
- 阅读源码:学习优秀开源项目中的联合类型使用
- 关注更新:TypeScript 不断改进联合类型相关特性
9.4 进一步学习