类型守卫与自定义守卫
00:00
类型守卫、自定义类型谓词
概述
类型守卫(Type Guard)是 TypeScript 在条件语句中收窄类型的机制。通过 typeof、instanceof、in 等内置运算符,TypeScript 能够在分支中自动推断更精确的类型。当内置守卫不够用时,可以通过自定义类型谓词(Type Predicate)和断言函数(Assertion Function)实现更复杂的类型收窄。类型守卫是编写类型安全代码的核心工具,掌握其使用方式对于处理联合类型和 unknown 类型的数据至关重要。
基础概念
typeof 守卫:使用 typeof 运算符检查原始类型(string、number、boolean、symbol、bigint、undefined、object、function)。适用于基本类型的区分。
instanceof 守卫:使用 instanceof 运算符检查实例类型。适用于类实例的区分,要求右侧是一个构造函数。
in 守卫:使用 in 运算符检查对象是否具有某个属性。适用于区分具有不同属性的对象类型。
自定义类型谓词:使用 parameterName is Type 作为函数返回值类型,告诉 TypeScript 在函数返回 true 时参数具有指定类型。
断言函数:使用 asserts parameterName is Type 或 asserts parameterName 作为函数返回值类型,在函数不抛出异常时保证类型收窄。
快速上手
内置类型守卫
// typeof 守卫
function process(value: string | number) {
if (typeof value === 'string') {
// 此处 value 类型收窄为 string
return value.toUpperCase();
}
// 此处 value 类型收窄为 number
return value.toFixed(2);
}
// instanceof 守卫
function handleError(error: Error | string) {
if (error instanceof TypeError) {
// error 类型收窄为 TypeError
console.log(error.message);
}
}
// in 守卫
interface Fish {
swim(): void;
}
interface Bird {
fly(): void;
}
function move(animal: Fish | Bird) {
if ('swim' in animal) {
// animal 类型收窄为 Fish
animal.swim();
} else {
// animal 类型收窄为 Bird
animal.fly();
}
}
自定义类型谓词
interface Dog {
bark(): void;
breed: string;
}
interface Cat {
meow(): void;
color: string;
}
type Pet = Dog | Cat;
// 自定义类型谓词:pet is Dog
function isDog(pet: Pet): pet is Dog {
return 'bark' in pet;
}
function interact(pet: Pet) {
if (isDog(pet)) {
// pet 类型收窄为 Dog
pet.bark();
console.log(pet.breed);
} else {
// pet 类型收窄为 Cat
pet.meow();
console.log(pet.color);
}
}
断言函数
// 断言值非空
function assertDefined<T>(
value: T | undefined | null,
message?: string
): asserts value is NonNullable<T> {
if (value == null) {
throw new Error(message ?? '值不能为空');
}
}
// 使用
const maybeUser: User | undefined = getUser();
assertDefined(maybeUser, '用户不存在');
// 此处 maybeUser 类型收窄为 User
// 断言条件为真
function assert(condition: boolean, message?: string): asserts condition {
if (!condition) {
throw new Error(message ?? '断言失败');
}
}
详细用法
通用守卫工具函数
// 字符串守卫
function isString(value: unknown): value is string {
return typeof value === 'string';
}
// 数字守卫(排除 NaN)
function isNumber(value: unknown): value is number {
return typeof value === 'number' && !isNaN(value);
}
// 对象守卫(排除 null 和数组)
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
// 数组守卫
function isArray<T>(value: unknown, itemGuard?: (item: unknown) => item is T): value is T[] {
if (!Array.isArray(value)) return false;
if (itemGuard) return value.every(itemGuard);
return true;
}
// 使用
function processValue(value: unknown) {
if (isString(value)) {
console.log(value.toUpperCase()); // value: string
} else if (isNumber(value)) {
console.log(value.toFixed(2)); // value: number
} else if (isObject(value)) {
console.log(Object.keys(value)); // value: Record<string, unknown>
}
}
嵌套类型守卫
// 验证 API 响应的结构
interface UserResponse {
id: number;
name: string;
email: string;
profile?: {
avatar: string;
bio: string;
};
}
function isUserResponse(value: unknown): value is UserResponse {
if (!isObject(value)) return false;
return (
isNumber(value.id) &&
isString(value.name) &&
isString(value.email) &&
(value.profile === undefined ||
(isObject(value.profile) && isString(value.profile.avatar) && isString(value.profile.bio)))
);
}
// 使用
function handleResponse(data: unknown) {
if (isUserResponse(data)) {
console.log(data.name); // data: UserResponse
if (data.profile) {
console.log(data.profile.avatar); // data.profile: { avatar: string; bio: string }
}
}
}
可辨识联合与守卫
// 使用 kind 属性作为辨识标签
interface Circle {
kind: 'circle';
radius: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
interface Triangle {
kind: 'triangle';
base: number;
height: number;
}
type Shape = Circle | Rectangle | Triangle;
// 使用 switch 进行类型收窄
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
// shape 类型收窄为 Circle
return Math.PI * shape.radius ** 2;
case 'rectangle':
// shape 类型收窄为 Rectangle
return shape.width * shape.height;
case 'triangle':
// shape 类型收窄为 Triangle
return 0.5 * shape.base * shape.height;
}
}
常见场景
API 响应验证
// 验证未知数据是否符合预期类型
function validateApiResponse(data: unknown): User[] {
if (!isArray(data)) {
throw new Error('响应不是数组');
}
return data
.filter((item): item is UserResponse => {
return isUserResponse(item);
})
.map((item) => ({
id: item.id,
name: item.name,
email: item.email,
}));
}
表单数据验证
interface FormData {
username: string;
password: string;
age: number;
agree: boolean;
}
function isFormData(value: unknown): value is FormData {
if (!isObject(value)) return false;
return (
isString(value.username) &&
isString(value.password) &&
isNumber(value.age) &&
typeof value.agree === 'boolean'
);
}
function submitForm(data: unknown) {
if (!isFormData(data)) {
throw new Error('表单数据格式错误');
}
// data 类型收窄为 FormData
console.log(data.username, data.age);
}
Redux/状态管理 Action
// 使用类型守卫区分 Action 类型
interface IncrementAction {
type: 'increment';
payload: { amount: number };
}
interface DecrementAction {
type: 'decrement';
payload: { amount: number };
}
interface ResetAction {
type: 'reset';
}
type CounterAction = IncrementAction | DecrementAction | ResetAction;
function isIncrementAction(action: CounterAction): action is IncrementAction {
return action.type === 'increment';
}
function isDecrementAction(action: CounterAction): action is DecrementAction {
return action.type === 'decrement';
}
function reducer(state: number, action: CounterAction): number {
if (isIncrementAction(action)) {
return state + action.payload.amount;
}
if (isDecrementAction(action)) {
return state - action.payload.amount;
}
return 0; // reset
}
注意事项
- 守卫函数的可靠性:自定义类型谓词不会在运行时验证类型,TypeScript 信任函数的返回值。如果守卫函数实现有误,会导致类型不安全。确保守卫逻辑与类型声明一致。
- 窄化范围:类型守卫只在当前作用域内有效。将守卫结果赋值给变量后,TypeScript 可能无法正确收窄类型,需要使用 const 或直接在条件中使用。
- 断言函数的异常:断言函数在条件不满足时必须抛出异常,否则 TypeScript 无法正确收窄类型。不要在断言函数中使用 return 代替 throw。
- unknown vs any:对于来自外部的数据(API 响应、用户输入等),使用 unknown 而非 any,然后通过类型守卫逐步收窄。这比 any 更安全。
进阶用法
链式守卫
// 链式类型守卫
function isNonNull<T>(value: T | null | undefined): value is T {
return value != null;
}
function isStringArray(value: unknown): value is string[] {
return isArray(value, isString);
}
// 组合使用
function processItems(data: unknown) {
if (isNonNull(data) && isStringArray(data)) {
// data 类型收窄为 string[]
data.forEach((item) => console.log(item.toUpperCase()));
}
}
泛型守卫
// 泛型类型守卫
function hasProperty<K extends string>(value: unknown, key: K): value is Record<K, unknown> {
return isObject(value) && key in value;
}
// 使用
function process(data: unknown) {
if (hasProperty(data, 'name') && isString(data.name)) {
console.log(data.name); // string
}
}
// 类型安全的属性访问
function getPropertySafe<T, K extends keyof T>(obj: T, key: K): T[K] | undefined {
return obj[key];
}
穷尽检查
// 使用 never 类型确保所有分支都被处理
type Result = 'success' | 'error' | 'pending';
function handleResult(result: Result) {
switch (result) {
case 'success':
return '操作成功';
case 'error':
return '操作失败';
case 'pending':
return '处理中';
default:
// 如果遗漏了某个分支,result 的类型不是 never,编译报错
const exhaustive: never = result;
return exhaustive;
}
}