TypeScript 联合类型:string | number 与类型保护
联合类型是 TypeScript 类型系统中极为实用的特性,它允许一个值在多种类型之间灵活切换。然而,有利必有弊——TypeScript 在处理联合类型时,只能访问所有类型共有的属性和方法。这时,类型保护机制应运而生,它让 TypeScript 能够在特定代码块中精确识别变量的具体类型。
1. 联合类型的基本概念
联合类型使用竖线 | 分隔多个类型,表示一个值可以是这些类型中的任意一种。想象一个场景:某个函数的参数既可能是字符串,也可能是数字。
function formatValue(value: string | number): string {
return String(value);
}
在这个例子中,value 的类型是 string | number,TypeScript 允许我们对这个变量调用所有类型共有的方法。String(value) 能够正常工作,因为无论 value 是字符串还是数字,都能转换为字符串。
再看一个更实际的例子。假设你正在开发一个配置文件解析器,某个配置项既可以是字符串类型,也可以是数组类型。
type ConfigValue = string | string[];
function processConfig(value: ConfigValue): void {
if (Array.isArray(value)) {
console.log(`收到数组,共 ${value.length} 个元素`);
} else {
console.log(`收到字符串:${value}`);
}
}
这段代码展示了联合类型的核心思想:同一个变量在不同场景下扮演不同角色。TypeScript 的类型系统会在背后默默跟踪这些可能的类型,并在代码执行过程中逐步缩小类型的范围。
2. 类型保护的必要性
当一个变量的类型是联合类型时,TypeScript 只能让我们访问所有类型共有的成员。一旦需要使用某个类型特有的属性或方法,编译器就会报错。
考虑以下场景:你有一个 User 类型,可能是普通用户,也可能是管理员。
interface BasicUser {
type: 'basic';
username: string;
email: string;
}
interface AdminUser {
type: 'admin';
username: string;
email: string;
permissions: string[];
}
type User = BasicUser | AdminUser;
如果你尝试直接访问 permissions 属性,TypeScript 会明确告诉你这个属性在 BasicUser 类型中不存在。
function printUserInfo(user: User): void {
// 错误:Property 'permissions' does not exist on type 'User'
console.log(user.permissions);
}
这就是类型保护发挥作用的地方。通过运行时检查,TypeScript 能够推断出在特定代码分支中变量的具体类型,从而允许访问该类型特有的属性。
3. typeof 类型守卫
typeof 是 JavaScript 原生的类型检查运算符,TypeScript 对其有专门的支持。当你在条件语句中使用 typeof 时,TypeScript 会自动缩小联合类型的范围。
function processInput(input: string | number): void {
if (typeof input === 'string') {
// 在这个分支中,input 被识别为 string 类型
console.log(`字符串长度:${input.length}`);
console.log(input.toUpperCase());
} else {
// 在 else 分支中,input 被识别为 number 类型
console.log(`数字值:${input.toFixed(2)}`);
console.log(Math.round(input));
}
}
typeof 类型守卫支持以下比较操作符:"string"、"number"、"bigint"、"boolean"、"symbol"、"undefined"、"object"、"function"。需要特别注意的是,typeof null 返回 "object",所以在使用 typeof 进行类型保护时要额外注意空值的情况。
function handleValue(value: string | null): void {
if (typeof value === 'string') {
console.log(`字符串内容:${value}`);
}
// 注意:typeof 无法区分 null 和 object
// 这里需要额外的 null 检查
}
```
---
## 4. 自定义类型守卫:类型谓词
当 `typeof` 无法满足需求时,可以定义自定义的类型守卫。这种方式通过返回类型谓词(type predicate)来告诉 TypeScript "这个变量在特定条件下就是某种类型"。
类型谓词的语法是 `parameterName is Type`,它是一个布尔函数,但其返回值会让 TypeScript 在调用处进行类型收窄。
假设你有一个数据解析场景,后端返回的数据可能是有效的 `User` 对象,也可能是解析失败的 `null`。
```typescript
interface User {
id: number;
name: string;
age: number;
}
interface RawData {
_tag: 'valid';
data: User;
}
interface InvalidData {
_tag: 'invalid';
error: string;
}
type ParsedResult = RawData | InvalidData;
function isValidUser(data: ParsedResult): data is RawData {
return data._tag === 'valid';
}
function handleParsedResult(result: ParsedResult): void {
if (isValidUser(result)) {
// 在这里,result 被识别为 RawData 类型
console.log(`有效用户:${result.data.name}`);
console.log(`用户ID:${result.data.id}`);
} else {
console.log(`解析失败:${result.error}`);
}
}
再看一个更复杂的例子。假设你需要从混合数组中筛选出特定类型的元素。
interface Dog {
species: 'dog';
name: string;
bark(): void;
}
interface Cat {
species: 'cat';
name: string;
meow(): void;
}
interface Bird {
species: 'bird';
name: string;
fly(): void;
}
type Pet = Dog | Cat | Bird;
function isDog(pet: Pet): pet is Dog {
return pet.species === 'dog';
}
function isCat(pet: Pet): pet is Cat {
return pet.species === 'cat';
}
function processPets(pets: Pet[]): void {
pets.forEach(pet => {
if (isDog(pet)) {
pet.bark();
} else if (isCat(pet)) {
pet.meow();
} else {
pet.fly();
}
});
}
5. in 操作符类型守卫
当对象类型包含可辨识的属性时,in 操作符可以用于类型保护。这种方式特别适合处理具有"标签"字段的对象。
interface Circle {
kind: 'circle';
radius: number;
}
interface Square {
kind: 'square';
sideLength: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
type Shape = Circle | Square | Rectangle;
function getArea(shape: Shape): number {
if ('radius' in shape) {
// shape 被识别为 Circle
return Math.PI * shape.radius ** 2;
}
if ('sideLength' in shape) {
// shape 被识别为 Square
return shape.sideLength ** 2;
}
// shape 被识别为 Rectangle
return shape.width * shape.height;
}
in 操作符在处理第三方库返回的类型时特别有用,因为这些类型可能没有明确的类型谓词定义。
6. instanceof 类型守卫
instanceof 检查对象的原型链,是另一种强大的类型保护机制。它适用于检查对象是否是某个类的实例。
class ApiResponse {
status: number;
data: unknown;
constructor(status: number, data: unknown) {
this.status = status;
this.data = data;
}
}
class ErrorResponse {
status: number;
message: string;
constructor(status: number, message: string) {
this.status = status;
this.message = message;
}
}
type Response = ApiResponse | ErrorResponse;
function handleResponse(response: Response): void {
if (response instanceof ErrorResponse) {
console.log(`错误响应:${response.message}`);
} else {
console.log(`API 响应状态:${response.status}`);
console.log(`数据:${JSON.stringify(response.data)}`);
}
}
```
`instanceof` 的优势在于它检查的是实际的原型关系,这使得它在处理类层次结构时特别可靠。
---
## 7. 可辨识联合与穷举检查
可辨识联合(Discriminated Union)是一种将类型保护与模式匹配结合的高级技巧。通过一个公共的可辨识属性(通常是字符串字面量),可以清晰地区分不同的类型变体。
```typescript
interface LoadingState {
status: 'loading';
progress: number;
}
interface SuccessState {
status: 'success';
data: string[];
}
interface ErrorState {
status: 'error';
message: string;
}
type AsyncState = LoadingState | SuccessState | ErrorState;
function renderState(state: AsyncState): string {
switch (state.status) {
case 'loading':
return `加载中:${state.progress}%`;
case 'success':
return `成功加载 ${state.data.length} 条记录`;
case 'error':
return `错误:${state.message}`;
default:
// 穷举检查:确保所有状态都被处理
const exhaustiveCheck: never = state;
return exhaustiveCheck;
}
}
这里的关键是 default 分支中的 never 类型赋值。如果后续添加了新的状态类型但忘记在 switch 中处理,TypeScript 会在编译时抛出错误,强制你完善所有分支。
8. 实际应用场景
在实际项目中,类型保护广泛用于数据处理、外部接口调用和表单验证等场景。
场景一:表单输入处理
interface TextInput {
type: 'text';
value: string;
placeholder: string;
}
interface NumberInput {
type: 'number';
value: number;
min: number;
max: number;
}
type InputField = TextInput | NumberInput;
function getInputValue(field: InputField): string {
if (field.type === 'text') {
return field.value;
} else {
return String(field.value);
}
}
function validateField(field: InputField): boolean {
if (field.type === 'number') {
return field.value >= field.min && field.value <= field.max;
}
return field.value.length > 0;
}
场景二:API 响应处理
interface ApiSuccess<T> {
success: true;
data: T;
meta?: {
page: number;
total: number;
};
}
interface ApiError {
success: false;
error: {
code: number;
message: string;
};
}
type ApiResponse<T> = ApiSuccess<T> | ApiError;
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
const data = await response.json();
return data;
}
async function useApiData<T>(url: string): Promise<T | null> {
const result = await fetchData<T>(url);
if (!result.success) {
console.error(`API 错误:${result.error.message}`);
return null;
}
return result.data;
}
```
**场景三:配置解析**
```typescript
interface ColorConfig {
mode: 'color';
hex: string;
alpha?: number;
}
interface PatternConfig {
mode: 'pattern';
url: string;
repeat: 'repeat' | 'no-repeat';
}
type StyleConfig = ColorConfig | PatternConfig;
function parseStyle(config: StyleConfig): string {
if (config.mode === 'color') {
const alpha = config.alpha ?? 1;
return `rgba(...)`; // 颜色处理逻辑
}
return `url(${config.url}) ${config.repeat}`;
}
9. 最佳实践总结
在项目中使用联合类型和类型保护时,遵循以下原则能让代码更加健壮和可维护:
| 场景 | 推荐方案 |
|---|---|
| 基础类型判断 | typeof 类型守卫 |
| 类实例判断 | instanceof 类型守卫 |
| 带有可辨识属性的对象 | 可辨识联合 + switch |
| 复杂类型判断 | 自定义类型谓词 |
| 确保所有分支被处理 | never 类型穷举检查 |
类型保护的核心价值在于它让 TypeScript 的类型系统在运行时也能发挥作用。通过合理的类型保护设计,你既能享受静态类型检查的安全性,又能在具体业务逻辑中获得精确的类型提示。这种能力在大型项目中尤为重要——它能帮助你及早发现潜在的 bug,同时大幅提升开发效率。

暂无评论,快来抢沙发吧!