类型守卫
问题
什么是 TypeScript 类型守卫(Type Guard)?如何使用类型守卫进行类型收窄?
答案
类型守卫是一种在运行时检查类型的技术,让 TypeScript 编译器能够在特定代码块中收窄(narrow)变量的类型。
为什么需要类型守卫
// 联合类型需要收窄后才能安全使用
function process(value: string | number) {
// value.toUpperCase(); // 错误:number 没有 toUpperCase 方法
// 需要先检查类型
if (typeof value === 'string') {
// 在这个块中,value 被收窄为 string
console.log(value.toUpperCase());
} else {
// 在这个块中,value 被收窄为 number
console.log(value.toFixed(2));
}
}
typeof 类型守卫
适用于原始类型检查:
function processValue(value: string | number | boolean | undefined) {
if (typeof value === 'string') {
// value: string
return value.toUpperCase();
}
if (typeof value === 'number') {
// value: number
return value.toFixed(2);
}
if (typeof value === 'boolean') {
// value: boolean
return value ? 'yes' : 'no';
}
// value: undefined
return 'undefined';
}
// typeof 只能检测这些类型
type TypeofResult =
| 'string'
| 'number'
| 'bigint'
| 'boolean'
| 'symbol'
| 'undefined'
| 'object' // 包括 null、数组、对象
| 'function';
注意
typeof null === 'object',这是 JavaScript 的历史遗留问题。需要单独检查 null。
instanceof 类型守卫
适用于类实例检查:
class Dog {
bark() {
console.log('Woof!');
}
}
class Cat {
meow() {
console.log('Meow!');
}
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
// animal: Dog
animal.bark();
} else {
// animal: Cat
animal.meow();
}
}
// 也适用于内置类
function processError(error: Error | string) {
if (error instanceof Error) {
console.log(error.message, error.stack);
} else {
console.log(error);
}
}
// 数组检查
function processArray(value: number[] | number) {
if (value instanceof Array) {
// 或者使用 Array.isArray(value)
return value.reduce((a, b) => a + b, 0);
}
return value;
}
in 操作符类型守卫
检查对象是否有某个属性:
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function move(animal: Bird | Fish) {
if ('fly' in animal) {
// animal: Bird
animal.fly();
} else {
// animal: Fish
animal.swim();
}
}
// 区分可选属性
interface A {
type: 'a';
name: string;
}
interface B {
type: 'b';
age: number;
}
function process(obj: A | B) {
if ('name' in obj) {
// obj: A
console.log(obj.name);
} else {
// obj: B
console.log(obj.age);
}
}
相等性类型守卫
使用 ===、!==、==、!= 收窄类型:
// 字面量类型收窄
function handle(x: 'a' | 'b' | 'c') {
if (x === 'a') {
// x: 'a'
} else if (x === 'b') {
// x: 'b'
} else {
// x: 'c'
}
}
// null/undefined 检查
function processNullable(value: string | null | undefined) {
if (value == null) {
// value: null | undefined(== null 同时匹配 null 和 undefined)
return 'empty';
}
// value: string
return value.toUpperCase();
}
// 比较两个变量
function compare(a: string | number, b: string | boolean) {
if (a === b) {
// a 和 b 都是 string(唯一可能相等的类型)
console.log(a.toUpperCase(), b.toUpperCase());
}
}
自定义类型守卫(is 关键字)
使用 is 关键字定义类型谓词:
interface User {
type: 'user';
name: string;
email: string;
}
interface Admin {
type: 'admin';
name: string;
permissions: string[];
}
// 自定义类型守卫函数
function isUser(person: User | Admin): person is User {
return person.type === 'user';
}
function isAdmin(person: User | Admin): person is Admin {
return person.type === 'admin';
}
function greet(person: User | Admin) {
if (isUser(person)) {
// person: User
console.log(`Hello, ${person.email}`);
} else {
// person: Admin
console.log(`Hello, Admin with ${person.permissions.length} permissions`);
}
}
// 检查数组元素
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function filterStrings(arr: unknown[]): string[] {
return arr.filter(isString);
}
复杂类型守卫
// 检查对象结构
interface ApiResponse<T> {
success: true;
data: T;
}
interface ApiError {
success: false;
error: string;
}
function isApiResponse<T>(
response: ApiResponse<T> | ApiError
): response is ApiResponse<T> {
return response.success === true;
}
async function fetchData(): Promise<ApiResponse<string> | ApiError> {
// ...
}
const result = await fetchData();
if (isApiResponse(result)) {
console.log(result.data); // string
} else {
console.log(result.error);
}
// 验证未知数据
function isUser2(data: unknown): data is User {
return (
typeof data === 'object' &&
data !== null &&
'type' in data &&
(data as any).type === 'user' &&
'name' in data &&
typeof (data as any).name === 'string' &&
'email' in data &&
typeof (data as any).email === 'string'
);
}
asserts 关键字(断言函数)
asserts 用于定义断言函数,如果检查失败则抛出错误:
// 断言函数
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Value must be a string');
}
}
function processValue(value: unknown) {
assertIsString(value);
// 此后 value 被断言为 string
console.log(value.toUpperCase());
}
// 断言非空
function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
if (value === null || value === undefined) {
throw new Error('Value must not be null or undefined');
}
}
function process(value: string | null) {
assertIsDefined(value);
// value: string
console.log(value.length);
}
// 断言条件
function assert(condition: boolean, message?: string): asserts condition {
if (!condition) {
throw new Error(message ?? 'Assertion failed');
}
}
function divide(a: number, b: number): number {
assert(b !== 0, 'Division by zero');
return a / b;
}
可辨识联合(Discriminated Unions)
使用共同的字面量属性区分类型:
// 定义可辨识联合
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;
// 使用类型守卫
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 (shape.base * shape.height) / 2;
}
}
// 穷尽检查
function assertNever(x: never): never {
throw new Error('Unexpected value: ' + x);
}
function getAreaExhaustive(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return (shape.base * shape.height) / 2;
default:
// 如果漏掉某个 case,这里会报编译错误
return assertNever(shape);
}
}
真值收窄(Truthiness Narrowing)
function processValue(value: string | null | undefined) {
if (value) {
// value: string(排除了 null、undefined、空字符串)
console.log(value.toUpperCase());
}
}
// && 运算符
function getLength(str: string | null): number {
return str && str.length || 0;
}
// 可选链配合
function getName(user: { name?: string } | null) {
// user?.name 的类型是 string | undefined
if (user?.name) {
// user.name: string
return user.name.toUpperCase();
}
return 'Unknown';
}
// 注意:0、''、false 等假值也会被过滤
function processNumber(value: number | null) {
if (value) {
// 这里 0 也被过滤掉了!
console.log(value);
}
}
// 更准确的检查
function processNumberCorrect(value: number | null) {
if (value !== null) {
// value: number(包括 0)
console.log(value);
}
}
类型守卫与泛型
// 泛型类型守卫
function isArray<T>(value: T | T[]): value is T[] {
return Array.isArray(value);
}
function process<T>(value: T | T[]): T[] {
if (isArray(value)) {
return value;
}
return [value];
}
// 类型安全的 filter
function isDefined<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
const arr = [1, null, 2, undefined, 3];
const filtered = arr.filter(isDefined); // number[]
// 类型谓词与映射
function isStringArray(arr: unknown[]): arr is string[] {
return arr.every((item) => typeof item === 'string');
}
常见面试问题
Q1: 类型守卫的几种方式?
答案:
| 方式 | 适用场景 | 示例 |
|---|---|---|
typeof | 原始类型 | typeof x === 'string' |
instanceof | 类实例 | x instanceof Date |
in | 属性检查 | 'name' in obj |
===/!== | 字面量、null/undefined | x === null |
| 自定义守卫 | 复杂类型 | function isUser(x): x is User |
asserts | 断言检查 | asserts x is string |
Q2: is 和 asserts 的区别?
答案:
// is - 返回布尔值,用于条件分支
function isString(value: unknown): value is string {
return typeof value === 'string';
}
if (isString(value)) {
// 分支内 value 是 string
}
// asserts - 不返回值,失败时抛出错误
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Not a string');
}
}
assertIsString(value);
// 之后 value 是 string
// 主要区别:
// - is: 返回 boolean,用于条件判断
// - asserts: 无返回值,失败抛异常,用于断言
Q3: 如何实现穷尽检查?
答案:
使用 never 类型确保所有情况都被处理:
type Action =
| { type: 'INCREMENT' }
| { type: 'DECREMENT' }
| { type: 'RESET' };
function assertNever(x: never): never {
throw new Error('Unexpected action: ' + JSON.stringify(x));
}
function reducer(state: number, action: Action): number {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
case 'RESET':
return 0;
default:
// 如果新增了 Action 类型但忘记处理,这里会报编译错误
return assertNever(action);
}
}
Q4: 如何校验未知数据的类型?
答案:
// 使用 zod 等验证库是更好的选择,手动实现如下:
interface User {
id: number;
name: string;
email: string;
}
function isUser(data: unknown): data is User {
if (typeof data !== 'object' || data === null) {
return false;
}
const obj = data as Record<string, unknown>;
return (
typeof obj.id === 'number' &&
typeof obj.name === 'string' &&
typeof obj.email === 'string'
);
}
// 使用
async function fetchUser(): Promise<User> {
const response = await fetch('/api/user');
const data: unknown = await response.json();
if (!isUser(data)) {
throw new Error('Invalid user data');
}
return data; // data: User
}
// 推荐使用 zod
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email()
});
type User = z.infer<typeof UserSchema>;
const result = UserSchema.safeParse(data);
if (result.success) {
const user: User = result.data;
}
Q5: typeof 和 instanceof 的局限性?
答案:
// typeof 的局限性
typeof null // 'object'(历史遗留 bug)
typeof [] // 'object'(无法区分数组)
typeof {} // 'object'
typeof new Date() // 'object'
// 需要额外检查
function isNull(value: unknown): value is null {
return value === null;
}
function isArray(value: unknown): value is unknown[] {
return Array.isArray(value);
}
// instanceof 的局限性
// 1. 跨 iframe 失效
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iframeArray = (iframe.contentWindow as any).Array;
const arr = new iframeArray();
arr instanceof Array; // false(不同全局环境)
// 2. 无法检查接口类型
interface User {
name: string;
}
// 没有办法用 instanceof 检查 User,因为 interface 在运行时不存在
// 解决方案:使用 in 或自定义类型守卫
function isUser(obj: unknown): obj is User {
return typeof obj === 'object' && obj !== null && 'name' in obj;
}