Chapter 11: TypeScript Integration

Haiyue
11min

Chapter 11: TypeScript Integration

Learning Objectives

  1. Master Axios TypeScript type definitions
  2. Learn to create type-safe API clients
  3. Implement interface type constraints
  4. Master advanced applications of generics
  5. Build HTTP services with complete typing

11.1 Basic Type Definitions

Axios TypeScript Basics

import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig } from 'axios';

// Basic type definitions
interface User {
  id: number;
  name: string;
  email: string;
  createdAt: string;
}

interface ApiResponse<T> {
  code: number;
  message: string;
  data: T;
}

// Basic usage
async function getUser(id: number): Promise<User> {
  const response: AxiosResponse<ApiResponse<User>> = await axios.get(`/api/users/${id}`);
  return response.data.data;
}

11.2 Generic API Client

Type-Safe API Client

class TypedApiClient {
  private client: AxiosInstance;

  constructor(baseURL: string, config?: AxiosRequestConfig) {
    this.client = axios.create({
      baseURL,
      ...config
    });

    this.setupInterceptors();
  }

  private setupInterceptors(): void {
    this.client.interceptors.response.use(
      (response) => response,
      (error) => {
        console.error('API error:', error);
        return Promise.reject(error);
      }
    );
  }

  // Generic GET method
  async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    const response: AxiosResponse<ApiResponse<T>> = await this.client.get(url, config);
    return response.data.data;
  }

  // Generic POST method
  async post<T, D = any>(url: string, data?: D, config?: AxiosRequestConfig): Promise<T> {
    const response: AxiosResponse<ApiResponse<T>> = await this.client.post(url, data, config);
    return response.data.data;
  }

  // Generic PUT method
  async put<T, D = any>(url: string, data?: D, config?: AxiosRequestConfig): Promise<T> {
    const response: AxiosResponse<ApiResponse<T>> = await this.client.put(url, data, config);
    return response.data.data;
  }

  // Generic DELETE method
  async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    const response: AxiosResponse<ApiResponse<T>> = await this.client.delete(url, config);
    return response.data.data;
  }
}

11.3 Service Layer Encapsulation

Typed Service Layer

// User-related types
interface CreateUserRequest {
  name: string;
  email: string;
  password: string;
}

interface UpdateUserRequest {
  name?: string;
  email?: string;
}

interface UserListParams {
  page?: number;
  limit?: number;
  search?: string;
}

interface PaginatedResponse<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
}

// User service class
class UserService {
  private api: TypedApiClient;

  constructor(api: TypedApiClient) {
    this.api = api;
  }

  async getUsers(params?: UserListParams): Promise<PaginatedResponse<User>> {
    return this.api.get<PaginatedResponse<User>>('/users', { params });
  }

  async getUser(id: number): Promise<User> {
    return this.api.get<User>(`/users/${id}`);
  }

  async createUser(userData: CreateUserRequest): Promise<User> {
    return this.api.post<User, CreateUserRequest>('/users', userData);
  }

  async updateUser(id: number, userData: UpdateUserRequest): Promise<User> {
    return this.api.put<User, UpdateUserRequest>(`/users/${id}`, userData);
  }

  async deleteUser(id: number): Promise<void> {
    return this.api.delete<void>(`/users/${id}`);
  }
}

11.4 Advanced Type Applications

Conditional Types and Mapped Types

// Response type conditional mapping
type ResponseData<T> = T extends string ? string :
                     T extends number ? number :
                     T extends boolean ? boolean :
                     T extends Array<infer U> ? U[] :
                     T;

// API endpoint type mapping
type ApiEndpoints = {
  '/users': {
    GET: { params?: UserListParams; response: PaginatedResponse<User> };
    POST: { data: CreateUserRequest; response: User };
  };
  '/users/:id': {
    GET: { response: User };
    PUT: { data: UpdateUserRequest; response: User };
    DELETE: { response: void };
  };
};

// Type-safe request function
type ExtractParams<T> = T extends { params: infer P } ? P : never;
type ExtractData<T> = T extends { data: infer D } ? D : never;
type ExtractResponse<T> = T extends { response: infer R } ? R : never;

class TypeSafeApiClient {
  private client: AxiosInstance;

  constructor(baseURL: string) {
    this.client = axios.create({ baseURL });
  }

  async request<
    Path extends keyof ApiEndpoints,
    Method extends keyof ApiEndpoints[Path]
  >(
    method: Method,
    path: Path,
    options?: {
      params?: ExtractParams<ApiEndpoints[Path][Method]>;
      data?: ExtractData<ApiEndpoints[Path][Method]>;
    }
  ): Promise<ExtractResponse<ApiEndpoints[Path][Method]>> {
    const response = await this.client.request({
      method: method as string,
      url: path as string,
      params: options?.params,
      data: options?.data
    });

    return response.data;
  }
}

11.5 Error Handling Types

Typed Error Handling

// Error type definitions
interface ApiError {
  code: string;
  message: string;
  details?: Record<string, any>;
}

interface ValidationError extends ApiError {
  code: 'VALIDATION_ERROR';
  fields: Record<string, string[]>;
}

interface AuthError extends ApiError {
  code: 'AUTH_ERROR';
  reason: 'INVALID_TOKEN' | 'TOKEN_EXPIRED' | 'INSUFFICIENT_PERMISSIONS';
}

// Typed error handler
class TypedErrorHandler {
  static handle(error: any): never {
    if (axios.isAxiosError(error)) {
      if (error.response) {
        const apiError = error.response.data as ApiError;

        switch (apiError.code) {
          case 'VALIDATION_ERROR':
            throw new ValidationErrorException(apiError as ValidationError);
          case 'AUTH_ERROR':
            throw new AuthErrorException(apiError as AuthError);
          default:
            throw new ApiErrorException(apiError);
        }
      } else if (error.request) {
        throw new NetworkErrorException(error.message);
      }
    }

    throw new UnknownErrorException(error.message);
  }
}

// Custom error classes
class ApiErrorException extends Error {
  constructor(public apiError: ApiError) {
    super(apiError.message);
    this.name = 'ApiErrorException';
  }
}

class ValidationErrorException extends ApiErrorException {
  constructor(public validationError: ValidationError) {
    super(validationError);
    this.name = 'ValidationErrorException';
  }
}

11.6 Decorator Pattern

Enhancing API with Decorators

// Cache decorator
function Cache(ttl: number = 300000) {
  return function(target: any, propertyName: string, descriptor: PropertyDescriptor) {
    const method = descriptor.value;
    const cache = new Map<string, { data: any; timestamp: number }>();

    descriptor.value = async function(...args: any[]) {
      const key = JSON.stringify(args);
      const cached = cache.get(key);

      if (cached && Date.now() - cached.timestamp < ttl) {
        console.log(`Cache hit: ${propertyName}`);
        return cached.data;
      }

      const result = await method.apply(this, args);
      cache.set(key, { data: result, timestamp: Date.now() });

      return result;
    };
  };
}

// Retry decorator
function Retry(maxAttempts: number = 3, delay: number = 1000) {
  return function(target: any, propertyName: string, descriptor: PropertyDescriptor) {
    const method = descriptor.value;

    descriptor.value = async function(...args: any[]) {
      let lastError: Error;

      for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
          return await method.apply(this, args);
        } catch (error) {
          lastError = error as Error;

          if (attempt === maxAttempts) break;

          console.log(`Retry ${propertyName} (${attempt}/${maxAttempts})`);
          await new Promise(resolve => setTimeout(resolve, delay * attempt));
        }
      }

      throw lastError;
    };
  };
}

// Service class using decorators
class EnhancedUserService {
  constructor(private api: TypedApiClient) {}

  @Cache(300000) // 5 minute cache
  async getUser(id: number): Promise<User> {
    return this.api.get<User>(`/users/${id}`);
  }

  @Retry(3, 1000) // Max 3 retries, 1 second interval
  async createUser(userData: CreateUserRequest): Promise<User> {
    return this.api.post<User>('/users', userData);
  }
}

11.7 Configuration Types

Strongly Typed Configuration

interface ApiClientConfig {
  baseURL: string;
  timeout?: number;
  auth?: {
    type: 'bearer' | 'basic' | 'api-key';
    credentials: string | { username: string; password: string };
  };
  retry?: {
    attempts: number;
    delay: number;
  };
  cache?: {
    enabled: boolean;
    ttl: number;
  };
}

class ConfigurableApiClient {
  private client: AxiosInstance;
  private config: Required<ApiClientConfig>;

  constructor(config: ApiClientConfig) {
    this.config = this.mergeWithDefaults(config);
    this.client = this.createAxiosInstance();
  }

  private mergeWithDefaults(config: ApiClientConfig): Required<ApiClientConfig> {
    return {
      baseURL: config.baseURL,
      timeout: config.timeout ?? 10000,
      auth: config.auth ?? { type: 'bearer', credentials: '' },
      retry: config.retry ?? { attempts: 3, delay: 1000 },
      cache: config.cache ?? { enabled: false, ttl: 300000 }
    };
  }

  private createAxiosInstance(): AxiosInstance {
    const instance = axios.create({
      baseURL: this.config.baseURL,
      timeout: this.config.timeout
    });

    this.setupAuth(instance);
    return instance;
  }

  private setupAuth(instance: AxiosInstance): void {
    if (this.config.auth.type === 'bearer') {
      instance.defaults.headers.common['Authorization'] =
        `Bearer ${this.config.auth.credentials}`;
    }
    // Other authentication methods...
  }
}

Chapter Summary

  • TypeScript provides complete type support for Axios
  • Generics make API clients type-safe
  • Interface definitions ensure data structure consistency
  • Decorator pattern enhances API functionality
  • Strongly typed configuration improves code quality

Key Points

  • Use generics to create reusable type-safe APIs
  • Interface definitions clarify data contracts
  • Error typing improves debugging efficiency
  • Decorators enhance functionality without breaking code structure