跳到主要内容

类型守卫

问题

什么是 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/undefinedx === 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;
}

相关链接