DevToolBox免费
博客

TypeScript 类型守卫:运行时类型检查完全指南

13 分钟作者 DevToolBox

TypeScript 提供了强大的类型系统,但类型在运行时会被擦除。类型守卫通过在运行时缩窄类型来弥补这一差距。本指南涵盖了从基本的 typeof 检查到高级判别联合和断言函数的所有类型守卫技术。

什么是类型守卫?

类型守卫是一种运行时检查,可以在条件块中缩窄变量的类型。TypeScript 理解这些检查并自动细化类型。

// Without type guard — TypeScript only knows 'value' is string | number
function process(value: string | number) {
  // value.toUpperCase(); // Error: Property 'toUpperCase' does not exist on type 'number'

  // With type guard — TypeScript narrows the type
  if (typeof value === 'string') {
    console.log(value.toUpperCase()); // OK: value is string here
  } else {
    console.log(value.toFixed(2));    // OK: value is number here
  }
}

typeof 类型守卫

typeof 运算符是最简单的类型守卫。它检查 JavaScript 运行时类型,TypeScript 会相应地缩窄类型。

function formatValue(value: string | number | boolean): string {
  if (typeof value === 'string') {
    // TypeScript knows: value is string
    return value.trim().toLowerCase();
  }
  if (typeof value === 'number') {
    // TypeScript knows: value is number
    return value.toLocaleString('en-US', { maximumFractionDigits: 2 });
  }
  // TypeScript knows: value is boolean
  return value ? 'Yes' : 'No';
}

// typeof works for these primitive types:
// 'string' | 'number' | 'bigint' | 'boolean'
// 'symbol' | 'undefined' | 'object' | 'function'

// Caveat: typeof null === 'object'
function processNullable(value: string | null) {
  if (typeof value === 'object') {
    // value could be null here!  typeof null === 'object'
  }
  if (value !== null && typeof value === 'object') {
    // Safe: value is not null
  }
}

instanceof 类型守卫

instanceof 运算符检查对象是否是某个类的实例。这对于错误处理和类层次结构特别有用。

class HttpError extends Error {
  constructor(public statusCode: number, message: string) {
    super(message);
    this.name = 'HttpError';
  }
}

class ValidationError extends Error {
  constructor(public field: string, message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

function handleError(error: Error) {
  if (error instanceof HttpError) {
    // TypeScript knows: error is HttpError
    console.log(`HTTP ${error.statusCode}: ${error.message}`);
    if (error.statusCode === 401) {
      redirectToLogin();
    }
  } else if (error instanceof ValidationError) {
    // TypeScript knows: error is ValidationError
    console.log(`Validation failed on field: ${error.field}`);
    highlightField(error.field);
  } else {
    // TypeScript knows: error is Error
    console.log(`Unexpected error: ${error.message}`);
  }
}

// instanceof with Date
function formatDate(input: string | Date): string {
  if (input instanceof Date) {
    return input.toISOString();
  }
  return new Date(input).toISOString();
}

"in" 运算符类型守卫

in 运算符检查属性是否存在于对象上。这对于区分共享部分属性的对象类型特别有用。

interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

function move(animal: Bird | Fish) {
  if ('fly' in animal) {
    // TypeScript knows: animal is Bird
    animal.fly();
  } else {
    // TypeScript knows: animal is Fish
    animal.swim();
  }
}

// Practical example: API responses
interface SuccessResponse {
  data: unknown;
  status: 'ok';
}

interface ErrorResponse {
  error: string;
  code: number;
}

function handleResponse(res: SuccessResponse | ErrorResponse) {
  if ('error' in res) {
    // TypeScript knows: res is ErrorResponse
    console.error(`Error ${res.code}: ${res.error}`);
  } else {
    // TypeScript knows: res is SuccessResponse
    processData(res.data);
  }
}

自定义类型守卫函数

自定义类型守卫是返回类型谓词的函数,让你将复杂的类型检查逻辑封装在可重用函数中。

// Type predicate syntax: paramName is Type
interface User {
  id: string;
  name: string;
  email: string;
}

interface Admin extends User {
  role: 'admin';
  permissions: string[];
}

// Custom type guard function
function isAdmin(user: User): user is Admin {
  return 'role' in user && (user as Admin).role === 'admin';
}

function showDashboard(user: User) {
  if (isAdmin(user)) {
    // TypeScript knows: user is Admin
    console.log(`Admin: ${user.name}, Permissions: ${user.permissions.join(', ')}`);
    renderAdminPanel(user.permissions);
  } else {
    // TypeScript knows: user is User (not Admin)
    console.log(`User: ${user.name}`);
    renderUserDashboard();
  }
}

// Type guard for checking non-null
function isNotNull<T>(value: T | null | undefined): value is T {
  return value !== null && value !== undefined;
}

// Use with .filter() to narrow array types
const items: (string | null)[] = ['hello', null, 'world', null];
const strings: string[] = items.filter(isNotNull);
// strings is string[] — no more nulls!

// Type guard for object shapes
function isValidUser(data: unknown): data is User {
  return (
    typeof data === 'object' &&
    data !== null &&
    'id' in data &&
    'name' in data &&
    'email' in data &&
    typeof (data as User).id === 'string' &&
    typeof (data as User).name === 'string' &&
    typeof (data as User).email === 'string'
  );
}

判别联合

判别联合使用共同的字面量属性来区分联合成员。这是 TypeScript 中最强大和类型安全的模式之一。

// The discriminant property: 'type'
interface Circle {
  type: 'circle';
  radius: number;
}

interface Rectangle {
  type: 'rectangle';
  width: number;
  height: number;
}

interface Triangle {
  type: 'triangle';
  base: number;
  height: number;
}

type Shape = Circle | Rectangle | Triangle;

function calculateArea(shape: Shape): number {
  switch (shape.type) {
    case 'circle':
      // TypeScript knows: shape is Circle
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      // TypeScript knows: shape is Rectangle
      return shape.width * shape.height;
    case 'triangle':
      // TypeScript knows: shape is Triangle
      return (shape.base * shape.height) / 2;
  }
}

// Real-world: Redux actions
type Action =
  | { type: 'FETCH_START' }
  | { type: 'FETCH_SUCCESS'; payload: User[] }
  | { type: 'FETCH_ERROR'; error: string }
  | { type: 'SET_FILTER'; filter: string };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true };
    case 'FETCH_SUCCESS':
      // TypeScript knows: action.payload exists and is User[]
      return { ...state, loading: false, users: action.payload };
    case 'FETCH_ERROR':
      // TypeScript knows: action.error exists and is string
      return { ...state, loading: false, error: action.error };
    case 'SET_FILTER':
      return { ...state, filter: action.filter };
  }
}

断言函数

断言函数通过在断言失败时抛出错误来缩窄类型。这种模式非常适合应用程序边界的验证。

// Assertion function syntax: asserts paramName is Type
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new TypeError(`Expected string, got ${typeof value}`);
  }
}

// Usage: type narrows AFTER the assertion call
function processInput(input: unknown) {
  assertIsString(input);
  // TypeScript knows: input is string from here on
  console.log(input.toUpperCase());
}

// Assert non-null
function assertDefined<T>(
  value: T | null | undefined,
  name: string
): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error(`${name} must be defined`);
  }
}

// Practical: Form validation
interface FormData {
  name: unknown;
  email: unknown;
  age: unknown;
}

function assertValidForm(data: FormData): asserts data is {
  name: string;
  email: string;
  age: number;
} {
  if (typeof data.name !== 'string' || data.name.length === 0) {
    throw new ValidationError('name', 'Name is required');
  }
  if (typeof data.email !== 'string' || !data.email.includes('@')) {
    throw new ValidationError('email', 'Valid email is required');
  }
  if (typeof data.age !== 'number' || data.age < 0 || data.age > 150) {
    throw new ValidationError('age', 'Valid age is required');
  }
}

function handleSubmit(data: FormData) {
  assertValidForm(data);
  // TypeScript knows all fields are properly typed here
  console.log(`Name: ${data.name}, Email: ${data.email}, Age: ${data.age}`);
}

控制流类型缩窄

TypeScript 执行控制流分析,根据赋值、相等性检查和真值性来缩窄类型。

真值缩窄

TypeScript 根据真值检查缩窄类型,过滤掉 null、undefined、0、空字符串和 false。

function greet(name: string | null | undefined) {
  if (name) {
    // TypeScript knows: name is string (not null/undefined)
    console.log(`Hello, ${name.toUpperCase()}!`);
  }
}

// Truthy narrowing with arrays
function processItems(items?: string[]) {
  if (items && items.length > 0) {
    // TypeScript knows: items is string[] (defined and non-empty)
    const first = items[0]; // string
  }
}

// Logical operators for narrowing
function getValue(a: string | null, b: string | null): string {
  // || narrows: returns first truthy value
  return a || b || 'default';
}

// Nullish coalescing for narrowing
function getConfig(config?: { timeout: number }) {
  const timeout = config?.timeout ?? 3000;
  // timeout is number (never undefined)
}

相等性缩窄

TypeScript 在使用 ===、!==、== 或 != 比较值时缩窄类型。

function example(x: string | number, y: string | boolean) {
  if (x === y) {
    // TypeScript knows: both x and y are string
    // (the only common type)
    console.log(x.toUpperCase());
    console.log(y.toUpperCase());
  }
}

// Equality narrowing with null
function process(value: string | null) {
  if (value !== null) {
    // TypeScript knows: value is string
    console.log(value.length);
  }
}

// Switch statement narrowing
function handleStatus(status: 'loading' | 'success' | 'error') {
  switch (status) {
    case 'loading':
      showSpinner();
      break;
    case 'success':
      hideSpinner();
      break;
    case 'error':
      showError();
      break;
  }
}

高级类型守卫模式

使用 never 的穷举检查

使用 never 类型确保处理了所有联合情况。

type Shape = Circle | Rectangle | Triangle;

function assertNever(x: never): never {
  throw new Error(`Unexpected value: ${x}`);
}

function getArea(shape: Shape): number {
  switch (shape.type) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      return shape.width * shape.height;
    case 'triangle':
      return (shape.base * shape.height) / 2;
    default:
      // If you add a new Shape variant and forget to handle it,
      // TypeScript will error here because shape won't be 'never'
      return assertNever(shape);
  }
}

// Alternative: satisfies + exhaustive check
function describeShape(shape: Shape): string {
  switch (shape.type) {
    case 'circle':    return `Circle with radius ${shape.radius}`;
    case 'rectangle': return `${shape.width}x${shape.height} rectangle`;
    case 'triangle':  return `Triangle with base ${shape.base}`;
    default: {
      const _exhaustive: never = shape;
      return _exhaustive;
    }
  }
}

泛型类型守卫

可以创建适用于任何类型的泛型类型守卫。

// Generic type guard for checking object keys
function hasProperty<K extends string>(
  obj: unknown,
  key: K
): obj is Record<K, unknown> {
  return typeof obj === 'object' && obj !== null && key in obj;
}

// Usage
function processData(data: unknown) {
  if (hasProperty(data, 'name') && hasProperty(data, 'age')) {
    console.log(data.name, data.age); // both are 'unknown' but accessible
  }
}

// Generic type guard for arrays
function isArrayOf<T>(
  arr: unknown,
  guard: (item: unknown) => item is T
): arr is T[] {
  return Array.isArray(arr) && arr.every(guard);
}

const isString = (value: unknown): value is string =>
  typeof value === 'string';

function handleInput(data: unknown) {
  if (isArrayOf(data, isString)) {
    // data is string[]
    data.forEach(s => console.log(s.toUpperCase()));
  }
}

数组类型守卫

使用 Array.isArray() 和类型安全的 filter 模式。

// Filtering with type guards
const mixed: (string | number | null)[] = ['a', 1, null, 'b', 2, null];

// Filter nulls with type predicate
const nonNull = mixed.filter(
  (item): item is string | number => item !== null
);
// nonNull: (string | number)[]

// Filter to specific type
const stringsOnly = mixed.filter(
  (item): item is string => typeof item === 'string'
);
// stringsOnly: string[]

// Type-safe .find()
const found = mixed.find(
  (item): item is string => typeof item === 'string'
);
// found: string | undefined

// Array.isArray() type guard
function flatten(input: string | string[]): string[] {
  if (Array.isArray(input)) {
    return input; // string[]
  }
  return [input]; // wrap single string in array
}

实际示例

API 响应处理

类型守卫对于在运行时验证 API 响应至关重要。

// API response types
interface ApiSuccess<T> {
  success: true;
  data: T;
}

interface ApiError {
  success: false;
  error: { message: string; code: string };
}

type ApiResponse<T> = ApiSuccess<T> | ApiError;

// Type guard based on discriminant
function isApiSuccess<T>(res: ApiResponse<T>): res is ApiSuccess<T> {
  return res.success === true;
}

// Usage with fetch
async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const json: ApiResponse<User> = await response.json();

  if (isApiSuccess(json)) {
    return json.data; // TypeScript knows: json.data is User
  } else {
    throw new Error(json.error.message);
  }
}

// Validate unknown API data
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 === 'string' &&
    typeof obj.name === 'string' &&
    typeof obj.email === 'string'
  );
}

事件处理

使用类型守卫缩窄 DOM 事件类型。

function handleEvent(event: Event) {
  if (event instanceof MouseEvent) {
    console.log(`Mouse at (${event.clientX}, ${event.clientY})`);
  } else if (event instanceof KeyboardEvent) {
    console.log(`Key pressed: ${event.key}`);
  } else if (event instanceof TouchEvent) {
    console.log(`Touch points: ${event.touches.length}`);
  }
}

// Input element type narrowing
function handleInput(event: Event) {
  const target = event.target;
  if (target instanceof HTMLInputElement) {
    console.log(target.value); // string
  } else if (target instanceof HTMLSelectElement) {
    console.log(target.selectedIndex); // number
  }
}

错误处理

类型守卫帮助你以类型安全的方式处理不同的错误类型。

// Type guard for unknown caught errors
function isError(error: unknown): error is Error {
  return error instanceof Error;
}

function getErrorMessage(error: unknown): string {
  if (isError(error)) return error.message;
  if (typeof error === 'string') return error;
  return 'An unknown error occurred';
}

// Safe error handling in async code
async function safeFetch(url: string) {
  try {
    const res = await fetch(url);
    if (!res.ok) throw new HttpError(res.status, res.statusText);
    return await res.json();
  } catch (error) {
    if (error instanceof HttpError) {
      handleHttpError(error); // error.statusCode available
    } else if (error instanceof TypeError) {
      handleNetworkError(error); // Network failure
    } else {
      handleUnknownError(getErrorMessage(error));
    }
  }
}

最佳实践

  1. 尽可能优先使用判别联合。
  2. 保持类型守卫函数简单。
  3. 在应用边界使用断言函数。
  4. 始终处理 else 情况或使用穷举检查。
  5. 在联合类型上使用 Array.isArray()。
  6. 组合多个类型守卫进行复杂缩窄。
  7. 彻底测试自定义类型守卫。

总结

TypeScript 类型守卫是连接编译时类型安全和运行时行为的必备工具。掌握这些模式,你可以编写既在编译时类型安全又在运行时健壮的代码。

常见问题

类型谓词和断言函数有什么区别?

类型谓词返回布尔值并在真值分支中缩窄类型。断言函数通过抛出错误来缩窄类型。

何时使用判别联合 vs 自定义类型守卫?

当你控制类型时使用判别联合。处理外部数据时使用自定义类型守卫。

类型守卫会导致运行时错误吗?

自定义类型守卫函数如果谓词逻辑与声明的类型不匹配,可能导致错误。

如何在数组中缩窄类型?

使用 Array.isArray() 和带类型谓词的 .filter()。

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

{ }JSON FormatterJSTypeScript to JavaScript

相关文章

TypeScript 泛型完全指南 2026:从基础到高级模式

全面掌握 TypeScript 泛型:类型参数、约束、条件类型、映射类型、工具类型,以及事件发射器和 API 客户端等实战模式。

TypeScript 工具类型速查表:Partial、Pick、Omit 等

TypeScript 工具类型完整参考与实际示例。学习 Partial、Required、Pick、Omit、Record、Exclude、Extract、ReturnType 和高级模式。

TypeScript vs JavaScript:何时使用哪个

TypeScript 和 JavaScript 的实用对比。类型安全、代码示例、迁移策略、性能、生态系统和选择指南。