阿里云主机折上折
  • 微信号
Current Site:Index > Type predicates and custom type guards

Type predicates and custom type guards

Author:Chuan Chen 阅读数:47105人阅读 分类: TypeScript

Type Predicates and Custom Type Guards

TypeScript's type system allows developers to enhance type inference capabilities through type predicates and custom type guards. These two mechanisms are particularly useful in complex type scenarios, helping the compiler understand type changes in the code.

Basic Concepts of Type Predicates

A type predicate is a special return type annotation in TypeScript, formatted as parameterName is Type. It is typically used in user-defined type guard functions to inform the compiler that if the function returns true, the parameter belongs to a specific type.

interface Cat {
  meow(): void;
}

interface Dog {
  bark(): void;
}

function isCat(animal: Cat | Dog): animal is Cat {
  return (animal as Cat).meow !== undefined;
}

In this example, the isCat function not only returns a boolean but also tells TypeScript through the animal is Cat predicate that when the function returns true, the parameter animal must be of type Cat.

Implementation of Custom Type Guards

Custom type guards are essentially functions that return type predicates. They can encapsulate complex type-checking logic, making the code clearer and type-safe.

type Primitive = string | number | boolean | symbol | null | undefined;

function isPrimitive(value: unknown): value is Primitive {
  return (
    typeof value === 'string' ||
    typeof value === 'number' ||
    typeof value === 'boolean' ||
    typeof value === 'symbol' ||
    value === null ||
    value === undefined
  );
}

function processValue(value: unknown) {
  if (isPrimitive(value)) {
    // Here, `value` is inferred as type `Primitive`
    console.log(value.toString());
  } else {
    // Here, `value` is inferred as type `object`
    console.log(Object.keys(value));
  }
}

Union Types and Type Guards

Type guards are especially useful when working with union types, as they help TypeScript narrow down the type range.

type Shape = 
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; sideLength: number }
  | { kind: 'triangle'; base: number; height: number };

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      // Here, `shape` is inferred as `{ kind: 'circle'; radius: number }`
      return Math.PI * shape.radius ** 2;
    case 'square':
      return shape.sideLength ** 2;
    case 'triangle':
      return (shape.base * shape.height) / 2;
  }
}

Examples of Complex Type Guards

For more complex scenarios, you can create type guards that combine multiple conditions.

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

interface User {
  role: 'user';
  lastLogin: Date;
}

type Person = Admin | User;

function isAdmin(person: Person): person is Admin {
  return person.role === 'admin';
}

function hasPermission(person: Person, permission: string): boolean {
  if (isAdmin(person)) {
    // Here, `person` is inferred as type `Admin`
    return person.permissions.includes(permission);
  }
  return false;
}

Advanced Usage of Type Predicates

Type predicates can be combined with generics to create more flexible type guards.

function isArrayOf<T>(
  arr: unknown,
  check: (item: unknown) => item is T
): arr is T[] {
  return Array.isArray(arr) && arr.every(check);
}

function isString(value: unknown): value is string {
  return typeof value === 'string';
}

const data: unknown = ['a', 'b', 'c'];

if (isArrayOf(data, isString)) {
  // Here, `data` is inferred as `string[]`
  data.forEach(s => console.log(s.toUpperCase()));
}

Runtime Type Checking and Type Guards

Type guards are often used to convert dynamic data (e.g., API responses) into static types.

interface ApiResponse<T> {
  success: boolean;
  data?: T;
  error?: string;
}

function isApiResponse<T>(
  response: unknown,
  checkData?: (data: unknown) => data is T
): response is ApiResponse<T> {
  if (typeof response !== 'object' || response === null) {
    return false;
  }
  
  const res = response as Record<string, unknown>;
  
  if (typeof res.success !== 'boolean') {
    return false;
  }
  
  if (res.success && checkData && 'data' in res) {
    return checkData(res.data);
  }
  
  return true;
}

// Usage example
const rawResponse = await fetch('/api/data');
const response = await rawResponse.json();

if (isApiResponse<string[]>(response, isArrayOf(isString))) {
  if (response.success) {
    console.log(response.data); // `string[]`
  } else {
    console.error(response.error);
  }
}

Performance Considerations for Type Guards

While type guards provide strong type safety, their runtime overhead should be considered.

// Simple but efficient type guard
function isNumber(value: unknown): value is number {
  return typeof value === 'number';
}

// Complex but potentially inefficient type guard
function isComplexObject(value: unknown): value is ComplexType {
  if (typeof value !== 'object' || value === null) return false;
  
  const obj = value as Record<string, unknown>;
  
  return (
    typeof obj.id === 'string' &&
    typeof obj.timestamp === 'number' &&
    Array.isArray(obj.items) &&
    // More complex checks...
  );
}

Differences Between Type Guards and Type Assertions

Type guards and type assertions (as) are fundamentally different: type guards perform runtime checks, while type assertions simply tell the compiler, "Trust me."

// Unsafe type assertion
const unsafeCast = someValue as string;

// Safe type guard
if (typeof someValue === 'string') {
  // Here, `someValue` is safely inferred as `string`
  const safeValue = someValue;
}

Limitations and Considerations for Type Predicates

Although powerful, type predicates have their limitations:

  1. Type predicates cannot check for non-existent properties.
  2. Overuse may complicate the code.
  3. Incorrectly implemented type guards may lead to runtime errors.
// Incorrect type guard implementation
function isBadGuard(value: unknown): value is string {
  // This implementation does not guarantee `value` is a string
  return true;
}

// Using an incorrect type guard
const test: unknown = 123;
if (isBadGuard(test)) {
  console.log(test.toUpperCase()); // Runtime error!
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.