T-CREATOR

TypeScript Function Overloads を使った柔軟な API 設計パターン

TypeScript Function Overloads を使った柔軟な API 設計パターン

TypeScript で API を設計する際に「引数のパターンによって戻り値の型を変えたい」「同じ関数名で異なる使い方をサポートしたい」といった要求に直面したことはありませんか?従来の JavaScript では実現が困難だったこれらの要求を、TypeScript の Function Overloads を使うことで型安全かつ直感的に解決できます。

本記事では、Function Overloads を活用した柔軟な API 設計パターンを詳しく解説します。ライブラリ開発者にとって必須のテクニックから、実際のプロジェクトで即座に活用できる実践的なパターンまで、あなたの TypeScript スキルを次のレベルに押し上げる知識をお届けします。

基本概念と設計思想

TypeScript の Function Overloads を効果的に活用するためには、まずその理論的背景と実装メカニズムを理解することが重要です。

Function Overloads の基本メカニズム

Function Overloads は、同じ関数名で異なるシグネチャを定義し、引数の型や数に応じて適切な戻り値の型を提供する仕組みです。

基本的な Overload の定義

typescript// 基本的な Function Overloads の例
function processData(data: string): string;
function processData(data: number): number;
function processData(data: boolean): boolean;
function processData(
  data: string | number | boolean
): string | number | boolean {
  if (typeof data === 'string') {
    return data.toUpperCase();
  } else if (typeof data === 'number') {
    return data * 2;
  } else {
    return !data;
  }
}

// 使用例:TypeScript が適切な戻り値の型を推論
const stringResult = processData('hello'); // string 型
const numberResult = processData(42); // number 型
const booleanResult = processData(true); // boolean 型

Overload シグネチャと実装シグネチャの関係

typescript// Overload シグネチャ(型定義のみ)
function createResource(id: string): Promise<Resource>;
function createResource(
  config: ResourceConfig
): Promise<Resource>;

// 実装シグネチャ(実際の処理)
function createResource(
  input: string | ResourceConfig
): Promise<Resource> {
  if (typeof input === 'string') {
    // ID から Resource を作成
    return fetch(`/api/resources/${input}`).then(
      (response) => response.json()
    );
  } else {
    // 設定オブジェクトから Resource を作成
    return fetch('/api/resources', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(input),
    }).then((response) => response.json());
  }
}

interface Resource {
  id: string;
  name: string;
  type: string;
}

interface ResourceConfig {
  name: string;
  type: string;
  metadata?: Record<string, unknown>;
}

引数パターンによる型推論の最適化

Function Overloads の真の価値は、引数の組み合わせに応じて最適な型推論を実現することにあります。

引数の有無による戻り値型の変化

typescript// 引数パターンによる型の最適化
interface QueryOptions {
  limit?: number;
  offset?: number;
  sort?: string;
}

interface User {
  id: string;
  name: string;
  email: string;
}

interface UserWithMetadata extends User {
  createdAt: Date;
  updatedAt: Date;
  loginCount: number;
}

// Overload による戻り値型の最適化
function getUsers(): Promise<User[]>;
function getUsers(options: QueryOptions): Promise<User[]>;
function getUsers(id: string): Promise<User | null>;
function getUsers(
  id: string,
  includeMetadata: true
): Promise<UserWithMetadata | null>;
function getUsers(
  idOrOptions?: string | QueryOptions,
  includeMetadata?: boolean
): Promise<User[] | User | UserWithMetadata | null> {
  if (typeof idOrOptions === 'string') {
    // 単一ユーザーの取得
    const userId = idOrOptions;
    if (includeMetadata) {
      return fetch(
        `/api/users/${userId}?include=metadata`
      ).then((response) =>
        response.json()
      ) as Promise<UserWithMetadata>;
    } else {
      return fetch(`/api/users/${userId}`).then(
        (response) => response.json()
      ) as Promise<User>;
    }
  } else {
    // ユーザーリストの取得
    const queryParams = new URLSearchParams();
    if (idOrOptions?.limit)
      queryParams.set('limit', String(idOrOptions.limit));
    if (idOrOptions?.offset)
      queryParams.set('offset', String(idOrOptions.offset));
    if (idOrOptions?.sort)
      queryParams.set('sort', idOrOptions.sort);

    return fetch(`/api/users?${queryParams}`).then(
      (response) => response.json()
    ) as Promise<User[]>;
  }
}

// 使用例:コンパイル時に適切な型が推論される
async function example() {
  const allUsers = await getUsers(); // Promise<User[]>
  const limitedUsers = await getUsers({ limit: 10 }); // Promise<User[]>
  const singleUser = await getUsers('user123'); // Promise<User | null>
  const userWithMeta = await getUsers('user123', true); // Promise<UserWithMetadata | null>
}

条件付き型との組み合わせ

typescript// より高度な型推論のパターン
type ApiResponse<
  T extends 'single' | 'list',
  U
> = T extends 'single' ? U | null : U[];

interface FetchOptions<T extends 'single' | 'list'> {
  type: T;
  params?: Record<string, string | number>;
}

// 条件付き型を活用した Overload
function fetchApi<T>(
  endpoint: string,
  options: FetchOptions<'single'>
): Promise<ApiResponse<'single', T>>;
function fetchApi<T>(
  endpoint: string,
  options: FetchOptions<'list'>
): Promise<ApiResponse<'list', T>>;
function fetchApi<T>(
  endpoint: string,
  options: FetchOptions<'single' | 'list'>
): Promise<T | T[] | null> {
  const { type, params } = options;
  const queryString = params
    ? '?' +
      new URLSearchParams(
        params as Record<string, string>
      ).toString()
    : '';

  return fetch(`${endpoint}${queryString}`)
    .then((response) => response.json())
    .then((data) => {
      if (type === 'single') {
        return data?.items?.[0] || null;
      } else {
        return data?.items || [];
      }
    });
}

// 使用例
async function advancedExample() {
  // 単一のアイテムを取得(T | null 型)
  const singleProduct = await fetchApi<Product>(
    '/products',
    {
      type: 'single',
      params: { id: '123' },
    }
  );

  // 複数のアイテムを取得(T[] 型)
  const productList = await fetchApi<Product>('/products', {
    type: 'list',
    params: { category: 'electronics' },
  });
}

コンパイラによる最適なシグネチャ選択

TypeScript コンパイラは、Overload の定義順序と引数の適合性を評価して、最適なシグネチャを選択します。

シグネチャの優先順位制御

typescript// シグネチャの順序が重要な例
interface ProcessingOptions {
  async?: boolean;
  timeout?: number;
}

// ❌ 問題のある Overload 順序
function badProcess(data: unknown): string;
function badProcess(
  data: string,
  options: ProcessingOptions
): string;
function badProcess(data: string): string; // 到達不可能
function badProcess(
  data: unknown,
  options?: ProcessingOptions
): string {
  // 実装
  return String(data);
}

// ✅ 適切な Overload 順序
function goodProcess(
  data: string,
  options: ProcessingOptions
): string;
function goodProcess(data: string): string;
function goodProcess(data: unknown): string;
function goodProcess(
  data: unknown,
  options?: ProcessingOptions
): string {
  if (typeof data === 'string' && options) {
    // 最も具体的なケース
    return options.async ? `async:${data}` : `sync:${data}`;
  } else if (typeof data === 'string') {
    // 中程度の具体性
    return `default:${data}`;
  } else {
    // 最も汎用的なケース
    return `generic:${String(data)}`;
  }
}

型ガードとの効果的な組み合わせ

typescript// 型ガードを活用した精密な Overload
interface DatabaseRecord {
  id: string;
  type: string;
}

interface UserRecord extends DatabaseRecord {
  type: 'user';
  name: string;
  email: string;
}

interface ProductRecord extends DatabaseRecord {
  type: 'product';
  name: string;
  price: number;
}

type AnyRecord = UserRecord | ProductRecord;

// 型ガード関数
function isUserRecord(
  record: AnyRecord
): record is UserRecord {
  return record.type === 'user';
}

function isProductRecord(
  record: AnyRecord
): record is ProductRecord {
  return record.type === 'product';
}

// 型ガードを活用した Overload
function processRecord(record: UserRecord): string;
function processRecord(record: ProductRecord): number;
function processRecord(record: AnyRecord): string | number {
  if (isUserRecord(record)) {
    return `User: ${record.name} (${record.email})`;
  } else if (isProductRecord(record)) {
    return record.price;
  } else {
    throw new Error('Unknown record type');
  }
}

// 使用例
const userRecord: UserRecord = {
  id: '1',
  type: 'user',
  name: 'John Doe',
  email: 'john@example.com',
};

const productRecord: ProductRecord = {
  id: '2',
  type: 'product',
  name: 'Laptop',
  price: 1000,
};

const userResult = processRecord(userRecord); // string 型
const productResult = processRecord(productRecord); // number 型

実用的な Overload パターン集

実際のプロジェクトで頻繁に使用される、具体的な Overload パターンを詳しく解説します。

条件分岐型 Overload(引数の有無による型変化)

引数の有無や組み合わせによって、戻り値の型や処理内容を変更するパターンです。

設定オブジェクトの有無による動作変更

typescript// 設定の有無による動作分岐
interface LoggerConfig {
  level: 'debug' | 'info' | 'warn' | 'error';
  format: 'json' | 'text';
  timestamp: boolean;
}

interface LogEntry {
  message: string;
  level: string;
  timestamp?: Date;
  formatted: string;
}

// 引数パターンによる戻り値型の変化
function createLogger(): (message: string) => void;
function createLogger(
  config: LoggerConfig
): (message: string) => LogEntry;
function createLogger(config?: LoggerConfig) {
  if (config) {
    // 設定ありの場合:詳細なログエントリを返す
    return (message: string): LogEntry => {
      const entry: LogEntry = {
        message,
        level: config.level,
        formatted:
          config.format === 'json'
            ? JSON.stringify({
                message,
                level: config.level,
              })
            : `[${config.level.toUpperCase()}] ${message}`,
      };

      if (config.timestamp) {
        entry.timestamp = new Date();
      }

      console.log(entry.formatted);
      return entry;
    };
  } else {
    // 設定なしの場合:シンプルなログ出力
    return (message: string): void => {
      console.log(message);
    };
  }
}

// 使用例
const simpleLogger = createLogger(); // (message: string) => void
const advancedLogger = createLogger({
  // (message: string) => LogEntry
  level: 'info',
  format: 'json',
  timestamp: true,
});

simpleLogger('Hello World'); // void
const logEntry = advancedLogger('Debug info'); // LogEntry

オプション引数による結果の型変更

typescript// オプション引数による型の細分化
interface SearchOptions {
  includeMetadata?: boolean;
  includeTags?: boolean;
  includeRelations?: boolean;
}

interface BaseItem {
  id: string;
  title: string;
  content: string;
}

interface ItemWithMetadata extends BaseItem {
  createdAt: Date;
  updatedAt: Date;
  author: string;
}

interface ItemWithTags extends BaseItem {
  tags: string[];
}

interface ItemWithRelations extends BaseItem {
  relatedItems: string[];
}

type EnrichedItem = BaseItem &
  Partial<ItemWithMetadata> &
  Partial<ItemWithTags> &
  Partial<ItemWithRelations>;

// オプションに応じた型推論
function searchItems(query: string): Promise<BaseItem[]>;
function searchItems(
  query: string,
  options: { includeMetadata: true }
): Promise<ItemWithMetadata[]>;
function searchItems(
  query: string,
  options: { includeTags: true }
): Promise<ItemWithTags[]>;
function searchItems(
  query: string,
  options: { includeRelations: true }
): Promise<ItemWithRelations[]>;
function searchItems(
  query: string,
  options: SearchOptions
): Promise<EnrichedItem[]>;
function searchItems(
  query: string,
  options?: SearchOptions
): Promise<
  | BaseItem[]
  | ItemWithMetadata[]
  | ItemWithTags[]
  | ItemWithRelations[]
  | EnrichedItem[]
> {
  const baseUrl = `/api/search?q=${encodeURIComponent(
    query
  )}`;
  const params = new URLSearchParams();

  if (options?.includeMetadata)
    params.append('include', 'metadata');
  if (options?.includeTags)
    params.append('include', 'tags');
  if (options?.includeRelations)
    params.append('include', 'relations');

  const url = params.toString()
    ? `${baseUrl}&${params}`
    : baseUrl;

  return fetch(url).then((response) => response.json());
}

// 使用例
async function searchExample() {
  const basicResults = await searchItems('typescript'); // BaseItem[]
  const withMeta = await searchItems('typescript', {
    includeMetadata: true,
  }); // ItemWithMetadata[]
  const withTags = await searchItems('typescript', {
    includeTags: true,
  }); // ItemWithTags[]
  const enriched = await searchItems('typescript', {
    includeMetadata: true,
    includeTags: true,
  }); // EnrichedItem[]
}

ジェネリック活用型 Overload(型パラメータとの組み合わせ)

ジェネリクスと Overload を組み合わせることで、より柔軟で型安全な API を設計できます。

型パラメータによる戻り値型の動的変更

typescript// ジェネリクスと Overload の組み合わせ
interface Serializer<T> {
  serialize(data: T): string;
  deserialize(data: string): T;
}

interface JsonSerializer extends Serializer<unknown> {
  type: 'json';
}

interface XmlSerializer extends Serializer<object> {
  type: 'xml';
}

interface BinarySerializer extends Serializer<ArrayBuffer> {
  type: 'binary';
}

// ジェネリック Overload の定義
function createSerializer<T>(type: 'json'): JsonSerializer;
function createSerializer<T extends object>(
  type: 'xml'
): XmlSerializer;
function createSerializer<T extends ArrayBuffer>(
  type: 'binary'
): BinarySerializer;
function createSerializer<T>(
  type: 'json' | 'xml' | 'binary'
): JsonSerializer | XmlSerializer | BinarySerializer {
  switch (type) {
    case 'json':
      return {
        type: 'json',
        serialize: (data: unknown) => JSON.stringify(data),
        deserialize: (data: string) => JSON.parse(data),
      } as JsonSerializer;

    case 'xml':
      return {
        type: 'xml',
        serialize: (data: object) => {
          // XML シリアライゼーション実装
          return `<root>${JSON.stringify(data)}</root>`;
        },
        deserialize: (data: string) => {
          // XML デシリアライゼーション実装
          return JSON.parse(data.replace(/<\/?root>/g, ''));
        },
      } as XmlSerializer;

    case 'binary':
      return {
        type: 'binary',
        serialize: (data: ArrayBuffer) => {
          return Array.from(new Uint8Array(data))
            .map((b) => b.toString(16).padStart(2, '0'))
            .join('');
        },
        deserialize: (data: string) => {
          const bytes =
            data
              .match(/.{2}/g)
              ?.map((byte) => parseInt(byte, 16)) || [];
          return new Uint8Array(bytes).buffer;
        },
      } as BinarySerializer;

    default:
      throw new Error(
        `Unsupported serializer type: ${type}`
      );
  }
}

// 使用例
const jsonSerializer = createSerializer('json'); // JsonSerializer
const xmlSerializer = createSerializer('xml'); // XmlSerializer
const binarySerializer = createSerializer('binary'); // BinarySerializer

制約付きジェネリクスでの型安全性確保

typescript// 制約付きジェネリクスを活用した高度な Overload
interface Validatable {
  validate(): boolean;
}

interface Persistable {
  save(): Promise<void>;
}

interface Entity extends Validatable, Persistable {
  id: string;
}

interface User extends Entity {
  name: string;
  email: string;
}

interface Product extends Entity {
  name: string;
  price: number;
}

// 制約付きジェネリクス Overload
function processEntity<T extends User>(
  entity: T,
  action: 'user-specific'
): Promise<T & { processed: true }>;
function processEntity<T extends Product>(
  entity: T,
  action: 'product-specific'
): Promise<T & { calculated: number }>;
function processEntity<T extends Entity>(
  entity: T,
  action: 'generic'
): Promise<T & { validated: boolean }>;
function processEntity<T extends Entity>(
  entity: T,
  action: 'user-specific' | 'product-specific' | 'generic'
): Promise<
  T &
    (
      | { processed: true }
      | { calculated: number }
      | { validated: boolean }
    )
> {
  const isValid = entity.validate();

  switch (action) {
    case 'user-specific':
      if ('email' in entity) {
        return entity.save().then(
          () =>
            ({
              ...entity,
              processed: true,
            } as T & { processed: true })
        );
      }
      break;

    case 'product-specific':
      if ('price' in entity) {
        return entity.save().then(
          () =>
            ({
              ...entity,
              calculated: (entity as Product).price * 1.1,
            } as T & { calculated: number })
        );
      }
      break;

    case 'generic':
      return entity.save().then(
        () =>
          ({
            ...entity,
            validated: isValid,
          } as T & { validated: boolean })
      );
  }

  throw new Error('Invalid entity or action combination');
}

Union 型活用 Overload(複数型の柔軟な受け入れ)

Union 型と Overload を組み合わせることで、多様な入力を型安全に処理できます。

入力型に応じた処理分岐

typescript// Union 型を活用した柔軟な入力処理
type InputData = string | number | object | Array<unknown>;

interface ProcessingResult<T> {
  input: T;
  type: string;
  result: unknown;
  timestamp: Date;
}

// Union 型に対応した Overload
function processInput(
  data: string
): ProcessingResult<string>;
function processInput(
  data: number
): ProcessingResult<number>;
function processInput(
  data: object
): ProcessingResult<object>;
function processInput(
  data: Array<unknown>
): ProcessingResult<Array<unknown>>;
function processInput(
  data: InputData
): ProcessingResult<InputData> {
  const timestamp = new Date();

  if (typeof data === 'string') {
    return {
      input: data,
      type: 'string',
      result: data.toUpperCase(),
      timestamp,
    };
  } else if (typeof data === 'number') {
    return {
      input: data,
      type: 'number',
      result: data * 2,
      timestamp,
    };
  } else if (Array.isArray(data)) {
    return {
      input: data,
      type: 'array',
      result: data.length,
      timestamp,
    };
  } else {
    return {
      input: data,
      type: 'object',
      result: Object.keys(data).length,
      timestamp,
    };
  }
}

// 使用例
const stringResult = processInput('hello'); // ProcessingResult<string>
const numberResult = processInput(42); // ProcessingResult<number>
const arrayResult = processInput([1, 2, 3]); // ProcessingResult<Array<unknown>>
const objectResult = processInput({ key: 'value' }); // ProcessingResult<object>

判別可能な Union 型での型安全性

typescript// 判別可能な Union 型を活用した Overload
interface ApiSuccessResponse {
  status: 'success';
  data: unknown;
}

interface ApiErrorResponse {
  status: 'error';
  error: {
    code: string;
    message: string;
  };
}

interface ApiLoadingResponse {
  status: 'loading';
  progress?: number;
}

type ApiResponse =
  | ApiSuccessResponse
  | ApiErrorResponse
  | ApiLoadingResponse;

// 判別可能な Union での Overload
function handleResponse(
  response: ApiSuccessResponse
): unknown;
function handleResponse(response: ApiErrorResponse): Error;
function handleResponse(
  response: ApiLoadingResponse
): Promise<unknown>;
function handleResponse(
  response: ApiResponse
): unknown | Error | Promise<unknown> {
  switch (response.status) {
    case 'success':
      console.log('Data received:', response.data);
      return response.data;

    case 'error':
      console.error('API Error:', response.error.message);
      return new Error(
        `${response.error.code}: ${response.error.message}`
      );

    case 'loading':
      console.log(
        'Loading...',
        response.progress ? `${response.progress}%` : ''
      );
      return new Promise((resolve) => {
        setTimeout(() => resolve(null), 1000);
      });

    default:
      const exhaustiveCheck: never = response;
      throw new Error(
        `Unhandled response type: ${exhaustiveCheck}`
      );
  }
}

Builder Pattern 風 Overload(段階的な設定)

Builder パターンのような段階的な設定を Overload で実現するパターンです。

段階的なオブジェクト構築

typescript// Builder Pattern を模した Overload
interface HttpRequestConfig {
  url: string;
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  headers?: Record<string, string>;
  body?: unknown;
  timeout?: number;
}

interface PartialConfig {
  url?: string;
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  headers?: Record<string, string>;
  body?: unknown;
  timeout?: number;
}

class HttpRequest {
  private config: PartialConfig = {};

  // URL 設定の Overload
  url(url: string): HttpRequest;
  url(): string | undefined;
  url(url?: string): HttpRequest | string | undefined {
    if (url !== undefined) {
      this.config.url = url;
      return this;
    }
    return this.config.url;
  }

  // メソッド設定の Overload
  method(
    method: 'GET' | 'POST' | 'PUT' | 'DELETE'
  ): HttpRequest;
  method(): string | undefined;
  method(
    method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
  ): HttpRequest | string | undefined {
    if (method !== undefined) {
      this.config.method = method;
      return this;
    }
    return this.config.method;
  }

  // ヘッダー設定の Overload
  headers(headers: Record<string, string>): HttpRequest;
  headers(key: string, value: string): HttpRequest;
  headers(): Record<string, string> | undefined;
  headers(
    keyOrHeaders?: string | Record<string, string>,
    value?: string
  ): HttpRequest | Record<string, string> | undefined {
    if (keyOrHeaders === undefined) {
      return this.config.headers;
    }

    if (typeof keyOrHeaders === 'string') {
      this.config.headers = this.config.headers || {};
      this.config.headers[keyOrHeaders] = value!;
      return this;
    } else {
      this.config.headers = {
        ...this.config.headers,
        ...keyOrHeaders,
      };
      return this;
    }
  }

  // リクエスト実行
  async execute(): Promise<Response> {
    if (!this.config.url) {
      throw new Error('URL is required');
    }

    const finalConfig: HttpRequestConfig = {
      url: this.config.url,
      method: this.config.method || 'GET',
      headers: this.config.headers,
      body: this.config.body,
      timeout: this.config.timeout || 5000,
    };

    return fetch(finalConfig.url, {
      method: finalConfig.method,
      headers: finalConfig.headers,
      body: finalConfig.body
        ? JSON.stringify(finalConfig.body)
        : undefined,
    });
  }
}

// 使用例
async function builderExample() {
  const response = await new HttpRequest()
    .url('/api/users')
    .method('POST')
    .headers({ 'Content-Type': 'application/json' })
    .headers('Authorization', 'Bearer token123')
    .execute();

  const data = await response.json();
  return data;
}

ライブラリ風 API 設計実践

実際のライブラリ開発で活用できる、Function Overloads を使った API 設計パターンを具体的に見ていきましょう。

データフェッチングライブラリの設計

現代の Web アプリケーションで必須のデータフェッチング機能を、Function Overloads で設計します。

多様なフェッチパターンへの対応

typescript// データフェッチングライブラリの設計
interface FetchOptions {
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  headers?: Record<string, string>;
  body?: unknown;
  timeout?: number;
  retry?: number;
}

interface FetchResponse<T> {
  data: T;
  status: number;
  headers: Record<string, string>;
}

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

class ApiClient {
  private baseUrl: string;
  private defaultHeaders: Record<string, string>;

  constructor(
    baseUrl: string,
    defaultHeaders: Record<string, string> = {}
  ) {
    this.baseUrl = baseUrl;
    this.defaultHeaders = defaultHeaders;
  }

  // 基本的な GET リクエスト
  get<T>(endpoint: string): Promise<FetchResponse<T>>;

  // オプション付き GET リクエスト
  get<T>(
    endpoint: string,
    options: FetchOptions
  ): Promise<FetchResponse<T>>;

  // ページネーション対応 GET リクエスト
  get<T>(
    endpoint: string,
    options: FetchOptions & { paginated: true }
  ): Promise<PaginatedResponse<T>>;

  async get<T>(
    endpoint: string,
    options?: FetchOptions & { paginated?: boolean }
  ): Promise<FetchResponse<T> | PaginatedResponse<T>> {
    const url = `${this.baseUrl}${endpoint}`;
    const config: RequestInit = {
      method: 'GET',
      headers: {
        ...this.defaultHeaders,
        ...options?.headers,
      },
    };

    const response = await this.executeRequest(
      url,
      config,
      options
    );
    const data = await response.json();

    if (options?.paginated) {
      return {
        data: data.items,
        pagination: {
          page: data.page || 1,
          limit: data.limit || 20,
          total: data.total || 0,
          hasNext: data.hasNext || false,
        },
      } as PaginatedResponse<T>;
    }

    return {
      data,
      status: response.status,
      headers: this.parseHeaders(response.headers),
    } as FetchResponse<T>;
  }

  // POST リクエストの Overload
  post<T>(
    endpoint: string,
    body: unknown
  ): Promise<FetchResponse<T>>;
  post<T>(
    endpoint: string,
    body: unknown,
    options: FetchOptions
  ): Promise<FetchResponse<T>>;
  async post<T>(
    endpoint: string,
    body: unknown,
    options?: FetchOptions
  ): Promise<FetchResponse<T>> {
    const url = `${this.baseUrl}${endpoint}`;
    const config: RequestInit = {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...this.defaultHeaders,
        ...options?.headers,
      },
      body: JSON.stringify(body),
    };

    const response = await this.executeRequest(
      url,
      config,
      options
    );
    const data = await response.json();

    return {
      data,
      status: response.status,
      headers: this.parseHeaders(response.headers),
    };
  }

  private async executeRequest(
    url: string,
    config: RequestInit,
    options?: FetchOptions
  ): Promise<Response> {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => {
      controller.abort();
    }, options?.timeout || 5000);

    try {
      const response = await fetch(url, {
        ...config,
        signal: controller.signal,
      });

      if (
        !response.ok &&
        options?.retry &&
        options.retry > 0
      ) {
        await new Promise((resolve) =>
          setTimeout(resolve, 1000)
        );
        return this.executeRequest(url, config, {
          ...options,
          retry: options.retry - 1,
        });
      }

      return response;
    } finally {
      clearTimeout(timeoutId);
    }
  }

  private parseHeaders(
    headers: Headers
  ): Record<string, string> {
    const result: Record<string, string> = {};
    headers.forEach((value, key) => {
      result[key] = value;
    });
    return result;
  }
}

// 使用例
async function fetchExample() {
  const client = new ApiClient('https://api.example.com');

  // 基本的な GET
  const user = await client.get<User>('/users/123');

  // オプション付き GET
  const userWithOptions = await client.get<User>(
    '/users/123',
    {
      headers: { Accept: 'application/json' },
      timeout: 10000,
    }
  );

  // ページネーション対応
  const users = await client.get<User>('/users', {
    paginated: true,
  });

  // POST リクエスト
  const newUser = await client.post<User>('/users', {
    name: 'John Doe',
    email: 'john@example.com',
  });
}

バリデーションライブラリの設計

型安全で柔軟なバリデーションライブラリを Function Overloads で設計します。

段階的バリデーション機能

typescript// バリデーションライブラリの設計
type ValidationRule<T> = (value: T) => boolean | string;
type ValidationResult = {
  isValid: boolean;
  errors: string[];
};

interface FieldValidator<T> {
  rules: ValidationRule<T>[];
  required: boolean;
}

class FormValidator {
  private validators: Map<string, FieldValidator<unknown>> =
    new Map();

  // 単一フィールドのバリデーション設定
  field<T>(name: string): FieldValidatorBuilder<T>;

  // 既存フィールドの取得
  field<T>(
    name: string,
    getter: true
  ): FieldValidator<T> | undefined;

  field<T>(
    name: string,
    getter?: boolean
  ):
    | FieldValidatorBuilder<T>
    | FieldValidator<T>
    | undefined {
    if (getter) {
      return this.validators.get(name) as
        | FieldValidator<T>
        | undefined;
    }
    return new FieldValidatorBuilder<T>(name, this);
  }

  // オブジェクト全体のバリデーション
  validate<T extends Record<string, unknown>>(
    data: T
  ): ValidationResult;

  // 特定フィールドのバリデーション
  validate<T>(
    fieldName: string,
    value: T
  ): ValidationResult;

  validate<T>(
    dataOrFieldName: T | string,
    value?: unknown
  ): ValidationResult {
    if (typeof dataOrFieldName === 'string') {
      // 単一フィールドのバリデーション
      return this.validateField(dataOrFieldName, value);
    } else {
      // オブジェクト全体のバリデーション
      return this.validateObject(
        dataOrFieldName as Record<string, unknown>
      );
    }
  }

  private validateField(
    fieldName: string,
    value: unknown
  ): ValidationResult {
    const validator = this.validators.get(fieldName);
    if (!validator) {
      return { isValid: true, errors: [] };
    }

    const errors: string[] = [];

    if (
      validator.required &&
      (value === undefined ||
        value === null ||
        value === '')
    ) {
      errors.push(`${fieldName} is required`);
      return { isValid: false, errors };
    }

    if (
      value !== undefined &&
      value !== null &&
      value !== ''
    ) {
      for (const rule of validator.rules) {
        const result = rule(value);
        if (result !== true) {
          errors.push(
            typeof result === 'string'
              ? result
              : `${fieldName} is invalid`
          );
        }
      }
    }

    return { isValid: errors.length === 0, errors };
  }

  private validateObject(
    data: Record<string, unknown>
  ): ValidationResult {
    const allErrors: string[] = [];

    for (const [fieldName, validator] of this.validators) {
      const fieldResult = this.validateField(
        fieldName,
        data[fieldName]
      );
      allErrors.push(...fieldResult.errors);
    }

    return {
      isValid: allErrors.length === 0,
      errors: allErrors,
    };
  }

  setValidator<T>(
    name: string,
    validator: FieldValidator<T>
  ): void {
    this.validators.set(
      name,
      validator as FieldValidator<unknown>
    );
  }
}

class FieldValidatorBuilder<T> {
  private fieldValidator: FieldValidator<T> = {
    rules: [],
    required: false,
  };

  constructor(
    private fieldName: string,
    private parentValidator: FormValidator
  ) {}

  // 必須フィールド設定
  required(): FieldValidatorBuilder<T>;
  required(isRequired: boolean): FieldValidatorBuilder<T>;
  required(
    isRequired: boolean = true
  ): FieldValidatorBuilder<T> {
    this.fieldValidator.required = isRequired;
    return this;
  }

  // カスタムルール追加
  rule(
    validator: ValidationRule<T>
  ): FieldValidatorBuilder<T>;
  rule(
    validator: ValidationRule<T>,
    errorMessage: string
  ): FieldValidatorBuilder<T>;
  rule(
    validator: ValidationRule<T>,
    errorMessage?: string
  ): FieldValidatorBuilder<T> {
    if (errorMessage) {
      this.fieldValidator.rules.push((value: T) => {
        const result = validator(value);
        return result === true ? true : errorMessage;
      });
    } else {
      this.fieldValidator.rules.push(validator);
    }
    return this;
  }

  // 文字列専用バリデーション(型制約付き)
  minLength(
    length: number
  ): T extends string ? FieldValidatorBuilder<T> : never {
    if (typeof '' as T extends string ? T : never) {
      this.fieldValidator.rules.push((value: T) => {
        const str = value as unknown as string;
        return (
          str.length >= length ||
          `Minimum length is ${length}`
        );
      });
    }
    return this as T extends string
      ? FieldValidatorBuilder<T>
      : never;
  }

  maxLength(
    length: number
  ): T extends string ? FieldValidatorBuilder<T> : never {
    if (typeof '' as T extends string ? T : never) {
      this.fieldValidator.rules.push((value: T) => {
        const str = value as unknown as string;
        return (
          str.length <= length ||
          `Maximum length is ${length}`
        );
      });
    }
    return this as T extends string
      ? FieldValidatorBuilder<T>
      : never;
  }

  // 数値専用バリデーション(型制約付き)
  min(
    minimum: number
  ): T extends number ? FieldValidatorBuilder<T> : never {
    if (typeof 0 as T extends number ? T : never) {
      this.fieldValidator.rules.push((value: T) => {
        const num = value as unknown as number;
        return (
          num >= minimum || `Minimum value is ${minimum}`
        );
      });
    }
    return this as T extends number
      ? FieldValidatorBuilder<T>
      : never;
  }

  max(
    maximum: number
  ): T extends number ? FieldValidatorBuilder<T> : never {
    if (typeof 0 as T extends number ? T : never) {
      this.fieldValidator.rules.push((value: T) => {
        const num = value as unknown as number;
        return (
          num <= maximum || `Maximum value is ${maximum}`
        );
      });
    }
    return this as T extends number
      ? FieldValidatorBuilder<T>
      : never;
  }

  // バリデーター登録
  build(): FormValidator {
    this.parentValidator.setValidator(
      this.fieldName,
      this.fieldValidator
    );
    return this.parentValidator;
  }
}

// 使用例
const validator = new FormValidator()
  .field<string>('name')
  .required()
  .minLength(2)
  .maxLength(50)
  .build()
  .field<string>('email')
  .required()
  .rule(
    (email: string) =>
      /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email),
    'Invalid email format'
  )
  .build()
  .field<number>('age')
  .required()
  .min(0)
  .max(120)
  .build();

// バリデーション実行
const userForm = {
  name: 'John',
  email: 'john@example.com',
  age: 25,
};

const result = validator.validate(userForm); // 全体バリデーション
const nameResult = validator.validate('name', 'J'); // 単一フィールドバリデーション

イベント処理ライブラリの設計

型安全なイベント処理を実現するライブラリを設計します。

型安全なイベントエミッター

typescript// イベント処理ライブラリの設計
type EventListener<T = unknown> = (
  data: T
) => void | Promise<void>;
type EventMap = Record<string, unknown>;

interface EventEmitterOptions {
  maxListeners?: number;
  async?: boolean;
}

class TypedEventEmitter<T extends EventMap> {
  private listeners: Map<
    keyof T,
    Set<EventListener<T[keyof T]>>
  > = new Map();
  private options: Required<EventEmitterOptions>;

  constructor(options: EventEmitterOptions = {}) {
    this.options = {
      maxListeners: options.maxListeners || 10,
      async: options.async || false,
    };
  }

  // イベントリスナー登録の Overload
  on<K extends keyof T>(
    event: K,
    listener: EventListener<T[K]>
  ): this;
  on<K extends keyof T>(
    event: K,
    listener: EventListener<T[K]>,
    options: { once?: boolean }
  ): this;
  on<K extends keyof T>(
    event: K,
    listener: EventListener<T[K]>,
    options: { once?: boolean } = {}
  ): this {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }

    const listenerSet = this.listeners.get(event)!;

    if (listenerSet.size >= this.options.maxListeners) {
      console.warn(
        `Max listeners (${
          this.options.maxListeners
        }) exceeded for event: ${String(event)}`
      );
    }

    if (options.once) {
      const onceWrapper: EventListener<T[K]> = (
        data: T[K]
      ) => {
        listenerSet.delete(onceWrapper);
        return listener(data);
      };
      listenerSet.add(onceWrapper);
    } else {
      listenerSet.add(listener);
    }

    return this;
  }

  // 一回限りのリスナー登録
  once<K extends keyof T>(
    event: K,
    listener: EventListener<T[K]>
  ): this {
    return this.on(event, listener, { once: true });
  }

  // イベント発火の Overload
  emit<K extends keyof T>(event: K, data: T[K]): boolean;
  emit<K extends keyof T>(
    event: K,
    data: T[K],
    options: { async: true }
  ): Promise<boolean>;
  emit<K extends keyof T>(
    event: K,
    data: T[K],
    options: { async?: boolean } = {}
  ): boolean | Promise<boolean> {
    const listenerSet = this.listeners.get(event);
    if (!listenerSet || listenerSet.size === 0) {
      return false;
    }

    const useAsync = options.async || this.options.async;

    if (useAsync) {
      return this.emitAsync(event, data, listenerSet);
    } else {
      return this.emitSync(event, data, listenerSet);
    }
  }

  private emitSync<K extends keyof T>(
    event: K,
    data: T[K],
    listenerSet: Set<EventListener<T[K]>>
  ): boolean {
    for (const listener of listenerSet) {
      try {
        listener(data);
      } catch (error) {
        console.error(
          `Error in listener for event ${String(event)}:`,
          error
        );
      }
    }
    return true;
  }

  private async emitAsync<K extends keyof T>(
    event: K,
    data: T[K],
    listenerSet: Set<EventListener<T[K]>>
  ): Promise<boolean> {
    const promises = Array.from(listenerSet).map(
      async (listener) => {
        try {
          await listener(data);
        } catch (error) {
          console.error(
            `Error in async listener for event ${String(
              event
            )}:`,
            error
          );
        }
      }
    );

    await Promise.all(promises);
    return true;
  }

  // リスナー削除の Overload
  off<K extends keyof T>(event: K): this;
  off<K extends keyof T>(
    event: K,
    listener: EventListener<T[K]>
  ): this;
  off<K extends keyof T>(
    event?: K,
    listener?: EventListener<T[K]>
  ): this {
    if (event === undefined) {
      // 全イベントのリスナーを削除
      this.listeners.clear();
    } else if (listener === undefined) {
      // 特定イベントの全リスナーを削除
      this.listeners.delete(event);
    } else {
      // 特定のリスナーを削除
      const listenerSet = this.listeners.get(event);
      if (listenerSet) {
        listenerSet.delete(listener);
        if (listenerSet.size === 0) {
          this.listeners.delete(event);
        }
      }
    }
    return this;
  }

  // リスナー数の取得
  listenerCount<K extends keyof T>(event?: K): number {
    if (event === undefined) {
      return Array.from(this.listeners.values()).reduce(
        (total, set) => total + set.size,
        0
      );
    }
    return this.listeners.get(event)?.size || 0;
  }
}

// 使用例
interface AppEvents {
  'user:login': { userId: string; timestamp: Date };
  'user:logout': { userId: string };
  'product:created': {
    productId: string;
    name: string;
    price: number;
  };
  error: { message: string; code: number };
}

const eventEmitter = new TypedEventEmitter<AppEvents>();

// 型安全なイベントリスナー登録
eventEmitter.on('user:login', (data) => {
  // data は { userId: string; timestamp: Date } 型
  console.log(
    `User ${data.userId} logged in at ${data.timestamp}`
  );
});

eventEmitter.once('product:created', async (data) => {
  // data は { productId: string; name: string; price: number } 型
  await saveToAnalytics(data);
});

// 型安全なイベント発火
eventEmitter.emit('user:login', {
  userId: 'user123',
  timestamp: new Date(),
});

// 非同期発火
await eventEmitter.emit(
  'product:created',
  {
    productId: 'prod456',
    name: 'New Product',
    price: 99.99,
  },
  { async: true }
);

エラーハンドリングと型安全性

Function Overloads を使用する際の適切なエラーハンドリングと、型安全性を保つためのベストプラクティスを解説します。

Overload 使用時の型エラー対策

適切なシグネチャ順序の設計

typescript// ❌ 問題のあるシグネチャ順序
function problematicOverload(input: unknown): string;
function problematicOverload(input: string): string; // 到達不可能
function problematicOverload(input: number): number; // 到達不可能

// ✅ 適切なシグネチャ順序
function correctOverload(input: string): string;
function correctOverload(input: number): number;
function correctOverload(input: boolean): boolean;
function correctOverload(
  input: unknown
): string | number | boolean;
function correctOverload(
  input: unknown
): string | number | boolean {
  if (typeof input === 'string') return input.toUpperCase();
  if (typeof input === 'number') return input * 2;
  if (typeof input === 'boolean') return !input;
  throw new Error('Unsupported input type');
}

型アサーションとガードの適切な使用

typescript// 型ガードを活用した安全な Overload
interface User {
  type: 'user';
  id: string;
  name: string;
}

interface Admin {
  type: 'admin';
  id: string;
  name: string;
  permissions: string[];
}

type UserOrAdmin = User | Admin;

// 型ガード関数
function isUser(entity: UserOrAdmin): entity is User {
  return entity.type === 'user';
}

function isAdmin(entity: UserOrAdmin): entity is Admin {
  return entity.type === 'admin';
}

// 型安全な Overload 実装
function processEntity(entity: User): string;
function processEntity(entity: Admin): string[];
function processEntity(
  entity: UserOrAdmin
): string | string[] {
  if (isUser(entity)) {
    return `User: ${entity.name}`;
  } else if (isAdmin(entity)) {
    return entity.permissions;
  } else {
    // never 型による網羅性チェック
    const exhaustiveCheck: never = entity;
    throw new Error(
      `Unhandled entity type: ${exhaustiveCheck}`
    );
  }
}

実行時エラーとコンパイル時エラーの使い分け

カスタムエラークラスの設計

typescript// Overload 専用のエラークラス
export class OverloadError extends Error {
  constructor(
    message: string,
    public readonly expectedTypes: string[],
    public readonly receivedType: string,
    public readonly receivedValue: unknown
  ) {
    super(message);
    this.name = 'OverloadError';
  }

  static createTypeError(
    functionName: string,
    expectedTypes: string[],
    receivedValue: unknown
  ): OverloadError {
    const receivedType = typeof receivedValue;
    const message =
      `${functionName}: Expected one of [${expectedTypes.join(
        ', '
      )}], ` +
      `but received ${receivedType}: ${receivedValue}`;

    return new OverloadError(
      message,
      expectedTypes,
      receivedType,
      receivedValue
    );
  }
}

// エラーハンドリングを含む Overload 実装
function safeProcessData(data: string): string;
function safeProcessData(data: number): number;
function safeProcessData(data: boolean): boolean;
function safeProcessData(
  data: string | number | boolean
): string | number | boolean {
  try {
    if (typeof data === 'string') {
      if (data.length === 0) {
        throw new OverloadError(
          'Empty string not allowed',
          ['non-empty string'],
          'string',
          data
        );
      }
      return data.toUpperCase();
    } else if (typeof data === 'number') {
      if (!Number.isFinite(data)) {
        throw new OverloadError(
          'Infinite or NaN numbers not allowed',
          ['finite number'],
          'number',
          data
        );
      }
      return data * 2;
    } else if (typeof data === 'boolean') {
      return !data;
    } else {
      throw OverloadError.createTypeError(
        'safeProcessData',
        ['string', 'number', 'boolean'],
        data
      );
    }
  } catch (error) {
    if (error instanceof OverloadError) {
      console.error('Overload Error:', error.message);
      throw error;
    }
    throw new OverloadError(
      `Unexpected error in safeProcessData: ${error}`,
      ['string', 'number', 'boolean'],
      typeof data,
      data
    );
  }
}

Result パターンによるエラーハンドリング

typescript// Result パターンを活用した型安全なエラーハンドリング
type Result<T, E = Error> =
  | {
      success: true;
      data: T;
    }
  | {
      success: false;
      error: E;
    };

// Result を返す Overload
function tryProcessData(
  data: string
): Result<string, OverloadError>;
function tryProcessData(
  data: number
): Result<number, OverloadError>;
function tryProcessData(
  data: boolean
): Result<boolean, OverloadError>;
function tryProcessData(
  data: string | number | boolean
): Result<string | number | boolean, OverloadError> {
  try {
    const result = safeProcessData(data);
    return { success: true, data: result };
  } catch (error) {
    return {
      success: false,
      error:
        error instanceof OverloadError
          ? error
          : new OverloadError(
              'Unknown error occurred',
              ['string', 'number', 'boolean'],
              typeof data,
              data
            ),
    };
  }
}

// 使用例
function handleResults() {
  const stringResult = tryProcessData('hello');
  if (stringResult.success) {
    console.log('String result:', stringResult.data); // string 型
  } else {
    console.error(
      'String error:',
      stringResult.error.message
    );
  }

  const numberResult = tryProcessData(42);
  if (numberResult.success) {
    console.log('Number result:', numberResult.data); // number 型
  } else {
    console.error(
      'Number error:',
      numberResult.error.message
    );
  }
}

デバッグしやすい Overload 設計

デバッグ情報の充実

typescript// デバッグ情報を含む Overload 設計
interface DebugInfo {
  functionName: string;
  selectedOverload: string;
  inputTypes: string[];
  timestamp: Date;
}

class OverloadTracker {
  private static instance: OverloadTracker;
  private callHistory: DebugInfo[] = [];

  static getInstance(): OverloadTracker {
    if (!OverloadTracker.instance) {
      OverloadTracker.instance = new OverloadTracker();
    }
    return OverloadTracker.instance;
  }

  track(info: DebugInfo): void {
    this.callHistory.push(info);
    if (this.callHistory.length > 100) {
      this.callHistory.shift(); // 履歴を100件に制限
    }
  }

  getHistory(): DebugInfo[] {
    return [...this.callHistory];
  }

  getLastCall(): DebugInfo | undefined {
    return this.callHistory[this.callHistory.length - 1];
  }
}

// デバッグ対応 Overload
function debuggableProcess(data: string): string;
function debuggableProcess(data: number): number;
function debuggableProcess(data: boolean): boolean;
function debuggableProcess(
  data: string | number | boolean
): string | number | boolean {
  const tracker = OverloadTracker.getInstance();
  let selectedOverload: string;
  let result: string | number | boolean;

  if (typeof data === 'string') {
    selectedOverload = 'string overload';
    result = data.toUpperCase();
  } else if (typeof data === 'number') {
    selectedOverload = 'number overload';
    result = data * 2;
  } else {
    selectedOverload = 'boolean overload';
    result = !data;
  }

  // デバッグ情報の記録
  tracker.track({
    functionName: 'debuggableProcess',
    selectedOverload,
    inputTypes: [typeof data],
    timestamp: new Date(),
  });

  return result;
}

// デバッグユーティリティ関数
function printOverloadHistory(): void {
  const tracker = OverloadTracker.getInstance();
  const history = tracker.getHistory();

  console.table(
    history.map((info) => ({
      Function: info.functionName,
      Overload: info.selectedOverload,
      InputTypes: info.inputTypes.join(', '),
      Timestamp: info.timestamp.toISOString(),
    }))
  );
}

// 使用例とデバッグ
debuggableProcess('hello');
debuggableProcess(42);
debuggableProcess(true);

printOverloadHistory(); // 実行履歴をテーブル形式で表示

まとめ:Function Overloads による開発体験の向上

TypeScript の Function Overloads は、従来の JavaScript では実現困難だった柔軟で型安全な API 設計を可能にする強力な機能です。本記事で紹介したパターンとベストプラクティスを活用することで、以下のような大きなメリットを実現できます。

Function Overloads の主要な価値

#価値具体的な効果適用シーン
1型安全性の向上引数パターンに応じた適切な戻り値型の推論ライブラリ API、ユーティリティ関数
2開発者体験の改善IDE での正確なオートコンプリートと型チェックチーム開発、大規模プロジェクト
3API の柔軟性確保単一の関数名で多様な使用パターンをサポート公開ライブラリ、フレームワーク
4保守性の向上明確な型定義による意図の伝達と変更の安全性長期保守プロジェクト
5学習コストの削減直感的な API 設計による使いやすさの向上新規メンバーの学習、ドキュメント化

実装における重要なポイント

1. 適切なシグネチャ設計

Function Overloads の効果を最大化するには、シグネチャの順序と具体性のバランスが重要です。より具体的なシグネチャから順番に定義し、最後に最も汎用的なシグネチャを配置することで、TypeScript コンパイラが適切なシグネチャを選択できます。

2. エラーハンドリングの戦略

型安全性を保ちながら、実行時エラーに対する適切な対応策を用意することが重要です。カスタムエラークラスや Result パターンを活用することで、エラー情報を構造化し、デバッグしやすい設計を実現できます。

3. パフォーマンスへの配慮

Function Overloads はコンパイル時の型チェック機能であり、実行時のパフォーマンスに直接的な影響は与えません。しかし、複雑な型推論や大量のシグネチャはコンパイル時間に影響する可能性があるため、適切なバランスを保つことが重要です。

今後の発展と活用領域

Function Overloads の技術は、TypeScript の進化と共にさらなる発展が期待されます:

  • AI アシスタントとの連携: より精密な型推論による開発支援の向上
  • フレームワーク統合: Next.js、React、Vue などでの標準的な活用パターンの確立
  • ツールチェーンの進化: ESLint、Prettier、TypeDoc などでの専用サポート

本記事で紹介した実践的なパターンを活用して、より型安全で保守性の高い TypeScript コードを実現してください。Function Overloads は、単なる技術的な機能を超えて、チーム全体の開発体験を根本的に改善する力を持っています。

あなたのプロジェクトでも、今日から Function Overloads を活用して、次世代の API 設計を体験してみてはいかがでしょうか。

関連リンク