ブログ記事

TypeScript型システム応用編2025 - 型安全なAPIクライアントの実装

TypeScriptの高度な型機能を活用して、Conditional TypesやTemplate Literal Typesを駆使した型安全なAPIクライアントを実装する方法を徹底解説。zodによるランタイム検証も紹介します。

Web開発
TypeScript 型システム API開発 フロントエンド バックエンド
TypeScript型システム応用編2025 - 型安全なAPIクライアントの実装のヒーロー画像

Typescript の型システムは、単なる型チェック以上の強力な機能を提供します。 本記事では、Conditional Types や Template Literal Types などの高度な型機能を活用し、 コンパイル時に型安全性を保証する api クライアントの実装方法を詳しく解説します。

この記事で学べること

  • Typescript の高度な型機能(Conditional Types、Template Literal Types、Mapped Types)
  • zod を使用したランタイム型検証の実装
  • OpenAPI 仕様からの型自動生成
  • 型安全な api クライアントの設計パターン
  • 実践的なエラーハンドリングの型定義

目次

  1. Typescript の高度な型機能の基礎
  2. Conditional Types による条件分岐型の実装
  3. Template Literal Types を活用した url 型の構築
  4. zod によるランタイム型検証
  5. OpenAPI 仕様からの型自動生成
  6. 型安全な api クライアントの実装
  7. エラーハンドリングの型定義
  8. まとめ

TypeScriptの高度な型機能の基礎

Typescript の型システムは、javascript 開発における型安全性を飛躍的に向上させます。 特に、api クライアントの実装においては、コンパイル時にエラーを検出できることで、 実行時エラーを大幅に削減できます。

TypeScript採用率(2025年調査) 85 %

Mapped Typesの基本

Mapped Types は、既存の型から新しい型を生成する強力な機能です。

// 基本的なMapped Type
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

// 使用例
interface User {
  id: number;
  name: string;
  email: string;
}

type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string; readonly email: string; }

プロのヒント

Mapped Types は、api レスポンスの型変換や、フォームデータの検証において特に有効です。 条件付きマッピングと組み合わせることで、より柔軟な型定義が可能になります。

Utility Typesの活用

Typescript には多くの組み込み Utility Types があり、これらを活用することで 型定義を簡潔かつ強力にできます。

主要なUtility Typesと活用シーン
Utility Type 説明 使用例 活用シーン
Partial<T> 全プロパティをオプショナルに Partial<User> 更新API
Required<T> 全プロパティを必須に Required<Config> 設定検証
Pick<T, K> 特定のプロパティを抽出 Pick<User, 'id' | 'name'> レスポンス型
Omit<T, K> 特定のプロパティを除外 Omit<User, 'password'> セキュリティ
Record<K, T> キーと値の型を定義 Record<string, User> マップ型

Conditional Typesによる条件分岐型の実装

Conditional Types は、型レベルでの条件分岐を可能にする機能です。 api クライアントの実装において、エンドポイントごとに異なる型を返す場合に特に有効です。

基本的なConditional Types

// 基本構文: T extends U ? X : Y
type IsString<T> = T extends string ? true : false;

// 使用例
type Test1 = IsString<string>;  // true
type Test2 = IsString<number>;  // false

実践的な活用例:APIレスポンス型

// エンドポイントごとに個別の型定義
interface GetUserResponse {
  id: number;
  name: string;
  email: string;
}

interface GetPostsResponse {
  posts: Array<{
    id: number;
    title: string;
    content: string;
  }>;
}

// APIクライアント
async function getUser(): Promise<GetUserResponse> {
  // 実装
}

async function getPosts(): Promise<GetPostsResponse> {
  // 実装
}
// エンドポイントと型をマッピング
type APIEndpoints = {
  '/users/:id': {
    response: {
      id: number;
      name: string;
      email: string;
    };
  };
  '/posts': {
    response: {
      posts: Array<{
        id: number;
        title: string;
        content: string;
      }>;
    };
  };
};

// Conditional Typesで型を推論
type APIResponse<T extends keyof APIEndpoints> = 
  APIEndpoints[T]['response'];

// 統一されたAPIクライアント
async function api<T extends keyof APIEndpoints>(
  endpoint: T
): Promise<APIResponse<T>> {
  // 実装
}
従来の実装
// エンドポイントごとに個別の型定義
interface GetUserResponse {
  id: number;
  name: string;
  email: string;
}

interface GetPostsResponse {
  posts: Array<{
    id: number;
    title: string;
    content: string;
  }>;
}

// APIクライアント
async function getUser(): Promise<GetUserResponse> {
  // 実装
}

async function getPosts(): Promise<GetPostsResponse> {
  // 実装
}
Conditional Typesを使った実装
// エンドポイントと型をマッピング
type APIEndpoints = {
  '/users/:id': {
    response: {
      id: number;
      name: string;
      email: string;
    };
  };
  '/posts': {
    response: {
      posts: Array<{
        id: number;
        title: string;
        content: string;
      }>;
    };
  };
};

// Conditional Typesで型を推論
type APIResponse<T extends keyof APIEndpoints> = 
  APIEndpoints[T]['response'];

// 統一されたAPIクライアント
async function api<T extends keyof APIEndpoints>(
  endpoint: T
): Promise<APIResponse<T>> {
  // 実装
}

inferキーワードの活用

infer キーワードを使用することで、Conditional Types 内で型を推論できます。

// 関数の戻り値の型を抽出
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// Promiseの中身の型を抽出
type Unpromise<T> = T extends Promise<infer U> ? U : T;

// 実践例:APIレスポンスの型抽出
type ExtractData<T> = T extends { data: infer D } ? D : never;

interface APIResponse<T> {
  data: T;
  status: number;
  message: string;
}

type UserData = ExtractData<APIResponse<User>>;  // User型が抽出される

Template Literal Typesを活用したURL型の構築

Template Literal Types は、文字列リテラル型を組み合わせて新しい型を生成する機能です。 api の url パターンを型安全に管理する際に非常に有効です。

基本的なTemplate Literal Types

// 基本構文
type Greeting<T extends string> = `Hello, ${T}!`;
type Message = Greeting<'World'>;  // "Hello, World!"

// HTTPメソッドとパスの組み合わせ
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type APIPath = '/users' | '/posts' | '/comments';
type APIRoute = `${HttpMethod} ${APIPath}`;
// "GET /users" | "GET /posts" | ... | "DELETE /comments"

動的URLパラメータの型定義

URLパラメータの型推論フロー

チャートを読み込み中...

// URLパラメータを抽出する型
type ExtractParams<T extends string> = 
  T extends `${infer _Start}:${infer Param}/${infer Rest}`
    ? { [K in Param]: string } & ExtractParams<Rest>
    : T extends `${infer _Start}:${infer Param}`
      ? { [K in Param]: string }
      : {};

// 使用例
type UserParams = ExtractParams<'/users/:userId/posts/:postId'>;
// { userId: string; postId: string }

// 型安全なURLビルダー
function buildUrl<T extends string>(
  pattern: T,
  params: ExtractParams<T>
): string {
  let url = pattern as string;
  Object.entries(params).forEach(([key, value]) => {
    url = url.replace(`:${key}`, String(value));
  });
  return url;
}

// 使用例(型エラーが発生する場合)
buildUrl('/users/:userId/posts/:postId', {
  userId: '123',
  // postId が不足しているためエラー
});

zodによるランタイム型検証

Typescript の型システムは強力ですが、コンパイル時のみの検証です。 api レスポンスなど、実行時のデータ検証には zod のようなランタイム検証ライブラリが必要です。

zodの基本的な使い方

import { z } from 'zod';

// スキーマの定義
const UserSchema = z.object({
  id: z.number(),
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().min(0).max(150).optional(),
  roles: z.array(z.string()),
  createdAt: z.string().datetime()
});

// TypeScriptの型を推論
type User = z.infer<typeof UserSchema>;

// 検証の実行
const validateUser = (data: unknown): User => {
  return UserSchema.parse(data);  // 失敗時は例外を投げる
};

// 安全な検証(例外を投げない)
const safeValidateUser = (data: unknown) => {
  return UserSchema.safeParse(data);
  // { success: true, data: User } | { success: false, error: ZodError }
};

注意

zod の parse メソッドは検証に失敗すると例外を投げます。 エラーハンドリングを行う場合は、safeParse メソッドを使用してください。

APIレスポンスの検証パターン

// APIレスポンスのスキーマ定義
const ApiResponseSchema = <T extends z.ZodType>(dataSchema: T) =>
  z.object({
    data: dataSchema,
    status: z.number(),
    message: z.string(),
    timestamp: z.string().datetime()
  });

// ユーザー一覧APIのレスポンススキーマ
const UsersResponseSchema = ApiResponseSchema(
  z.array(UserSchema)
);

// API呼び出しと検証
async function fetchUsers() {
  const response = await fetch('/api/users');
  const json = await response.json();
  
  // ランタイム検証
  const validated = UsersResponseSchema.parse(json);
  return validated.data;  // User[]型として扱える
}
// エラーレスポンスのスキーマ
const ErrorResponseSchema = z.object({
  error: z.object({
    code: z.string(),
    message: z.string(),
    details: z.array(z.object({
      field: z.string(),
      message: z.string()
    })).optional()
  }),
  status: z.number(),
  timestamp: z.string().datetime()
});

// ユニオン型でエラーも含む
const ApiResultSchema = <T extends z.ZodType>(dataSchema: T) =>
  z.union([
    ApiResponseSchema(dataSchema),
    ErrorResponseSchema
  ]);

// 型安全なエラーハンドリング
async function safeFetchUser(id: string) {
  try {
    const response = await fetch(`/api/users/${id}`);
    const json = await response.json();
    
    const result = ApiResultSchema(UserSchema).parse(json);
    
    if ('error' in result) {
      console.error('API Error:', result.error);
      return null;
    }
    
    return result.data;
  } catch (error) {
    if (error instanceof z.ZodError) {
      console.error('Validation Error:', error.errors);
    }
    return null;
  }
}
// カスタムバリデーションの追加
const PasswordSchema = z
  .string()
  .min(8, 'パスワードは8文字以上必要です')
  .refine(
    (password) => /[A-Z]/.test(password),
    'パスワードには大文字を含める必要があります'
  )
  .refine(
    (password) => /[0-9]/.test(password),
    'パスワードには数字を含める必要があります'
  )
  .refine(
    (password) => /[!@#$%^&*]/.test(password),
    'パスワードには特殊文字を含める必要があります'
  );

// 相互依存するフィールドの検証
const RegistrationSchema = z.object({
  email: z.string().email(),
  password: PasswordSchema,
  confirmPassword: z.string()
}).refine(
  (data) => data.password === data.confirmPassword,
  {
    message: 'パスワードが一致しません',
    path: ['confirmPassword']
  }
);

// 非同期バリデーション
const UniqueEmailSchema = z.string().email().refine(
  async (email) => {
    const response = await fetch(`/api/check-email?email=${email}`);
    const { exists } = await response.json();
    return !exists;
  },
  {
    message: 'このメールアドレスは既に使用されています'
  }
);

OpenAPI仕様からの型自動生成

OpenAPI(Swagger)仕様から typescript の型を自動生成することで、 api とクライアントの型定義を常に同期させることができます。

型生成ツールの比較

OpenAPI型生成ツールの比較(2025年版)
ツール 特徴 zodサポート カスタマイズ性
openapi-typescript シンプルで高速
openapi-typescript-codegen クライアントコード生成
zodios zod統合
orval 多機能・高カスタマイズ 非常に高

openapi-typescriptの実装例

// 1. OpenAPI仕様から型を生成
// npx openapi-typescript ./openapi.yaml -o ./src/types/api.ts

// 2. 生成された型をインポート
import { paths } from './types/api';

// 3. 型安全なAPIクライアントの実装
type ApiPaths = keyof paths;
type ApiMethods<P extends ApiPaths> = keyof paths[P];

type ApiRequest<
  P extends ApiPaths,
  M extends ApiMethods<P>
> = paths[P][M] extends { requestBody?: { content: { 'application/json': infer R } } }
  ? R
  : never;

type ApiResponse<
  P extends ApiPaths,
  M extends ApiMethods<P>
> = paths[P][M] extends { responses: { 200: { content: { 'application/json': infer R } } } }
  ? R
  : never;

// 型安全なfetch wrapper
async function apiCall<
  P extends ApiPaths,
  M extends ApiMethods<P>
>(
  path: P,
  method: M,
  options?: {
    body?: ApiRequest<P, M>;
    params?: Record<string, string>;
  }
): Promise<ApiResponse<P, M>> {
  const url = new URL(path as string, process.env.API_BASE_URL);
  
  if (options?.params) {
    Object.entries(options.params).forEach(([key, value]) => {
      url.searchParams.append(key, value);
    });
  }
  
  const response = await fetch(url.toString(), {
    method: method as string,
    headers: {
      'Content-Type': 'application/json',
    },
    body: options?.body ? JSON.stringify(options.body) : undefined,
  });
  
  if (!response.ok) {
    throw new Error(`API Error: ${response.status}`);
  }
  
  return response.json();
}

orvalによる高度な型生成

// orval.config.ts
import { defineConfig } from 'orval';

export default defineConfig({
  petstore: {
    input: {
      target: './openapi.yaml',
    },
    output: {
      mode: 'tags-split',
      target: './src/api',
      schemas: './src/models',
      client: 'react-query',
      mock: true,
      override: {
        mutator: {
          path: './src/api/custom-instance.ts',
          name: 'customInstance',
        },
        operations: {
          // 各エンドポイントのカスタマイズ
          listPets: {
            query: {
              useQuery: true,
              useInfinite: true,
              useInfiniteQueryParam: 'page',
            },
          },
        },
        // zodスキーマの生成
        zod: {
          generate: true,
          coerce: {
            dates: true,
            arrays: true,
          },
        },
      },
    },
  },
});

OpenAPI 3.1リリース

JSON Schema完全互換

TypeScript 5.0

装飾子の標準化

型生成ツール成熟期

zodとの統合が標準に

型安全なAPIクライアントの実装

これまでの要素を組み合わせて、完全に型安全な api クライアントを実装します。

基本設計

APIクライアントのアーキテクチャ

チャートを読み込み中...

完全な実装例

// api-client.ts
import { z } from 'zod';
import { paths } from './generated/api-types';

// 設定の型定義
interface ApiConfig {
  baseUrl: string;
  headers?: Record<string, string>;
  timeout?: number;
  retry?: {
    count: number;
    delay: number;
  };
}

// APIエラーの型定義
class ApiError extends Error {
  constructor(
    public status: number,
    public code: string,
    message: string,
    public details?: unknown
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

// 型ヘルパー
type PathParams<T extends string> = T extends `${infer _Start}{${infer Param}}${infer Rest}`
  ? { [K in Param]: string | number } & PathParams<Rest>
  : {};

type QueryParams<P extends keyof paths, M extends keyof paths[P]> =
  paths[P][M] extends { parameters?: { query?: infer Q } } ? Q : never;

type RequestBody<P extends keyof paths, M extends keyof paths[P]> =
  paths[P][M] extends { requestBody?: { content: { 'application/json': infer B } } } ? B : never;

type ResponseBody<P extends keyof paths, M extends keyof paths[P]> =
  paths[P][M] extends { responses: { 200: { content: { 'application/json': infer R } } } } ? R : never;

// APIクライアントクラス
export class TypeSafeApiClient {
  constructor(private config: ApiConfig) {}

  private async request<T>(
    url: string,
    options: RequestInit,
    schema?: z.ZodType<T>
  ): Promise<T> {
    const controller = new AbortController();
    const timeout = this.config.timeout || 30000;

    const timeoutId = setTimeout(() => controller.abort(), timeout);

    try {
      const response = await fetch(url, {
        ...options,
        signal: controller.signal,
        headers: {
          'Content-Type': 'application/json',
          ...this.config.headers,
          ...options.headers,
        },
      });

      clearTimeout(timeoutId);

      if (!response.ok) {
        const error = await response.json().catch(() => ({}));
        throw new ApiError(
          response.status,
          error.code || 'UNKNOWN_ERROR',
          error.message || response.statusText,
          error.details
        );
      }

      const data = await response.json();

      // zodスキーマが提供されている場合は検証
      if (schema) {
        return schema.parse(data);
      }

      return data as T;
    } catch (error) {
      clearTimeout(timeoutId);

      if (error instanceof ApiError) {
        throw error;
      }

      if (error instanceof z.ZodError) {
        throw new ApiError(
          422,
          'VALIDATION_ERROR',
          'レスポンスの検証に失敗しました',
          error.errors
        );
      }

      throw new ApiError(
        0,
        'NETWORK_ERROR',
        error instanceof Error ? error.message : 'ネットワークエラーが発生しました'
      );
    }
  }

  // 汎用的なAPIメソッド
  async call<P extends keyof paths, M extends keyof paths[P]>(
    path: P,
    method: M,
    options?: {
      params?: PathParams<P & string>;
      query?: QueryParams<P, M>;
      body?: RequestBody<P, M>;
      schema?: z.ZodType<ResponseBody<P, M>>;
    }
  ): Promise<ResponseBody<P, M>> {
    // URLの構築
    let url = `${this.config.baseUrl}${path as string}`;

    // パスパラメータの置換
    if (options?.params) {
      Object.entries(options.params).forEach(([key, value]) => {
        url = url.replace(`{${key}}`, String(value));
      });
    }

    // クエリパラメータの追加
    if (options?.query) {
      const params = new URLSearchParams();
      Object.entries(options.query).forEach(([key, value]) => {
        if (value !== undefined && value !== null) {
          params.append(key, String(value));
        }
      });
      const queryString = params.toString();
      if (queryString) {
        url += `?${queryString}`;
      }
    }

    return this.request<ResponseBody<P, M>>(
      url,
      {
        method: method as string,
        body: options?.body ? JSON.stringify(options.body) : undefined,
      },
      options?.schema
    );
  }

  // 便利メソッド
  get<P extends keyof paths>(
    path: P,
    options?: Omit<Parameters<typeof this.call>[2], 'body'>
  ) {
    return this.call(path, 'get' as any, options);
  }

  post<P extends keyof paths>(
    path: P,
    body?: RequestBody<P, 'post'>,
    options?: Omit<Parameters<typeof this.call>[2], 'body'>
  ) {
    return this.call(path, 'post' as any, { ...options, body });
  }

  put<P extends keyof paths>(
    path: P,
    body?: RequestBody<P, 'put'>,
    options?: Omit<Parameters<typeof this.call>[2], 'body'>
  ) {
    return this.call(path, 'put' as any, { ...options, body });
  }

  delete<P extends keyof paths>(
    path: P,
    options?: Omit<Parameters<typeof this.call>[2], 'body'>
  ) {
    return this.call(path, 'delete' as any, options);
  }
}

使用例

// スキーマ定義
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  createdAt: z.string().datetime(),
});

const UsersListSchema = z.array(UserSchema);

// クライアントの初期化
const apiClient = new TypeSafeApiClient({
  baseUrl: 'https://api.example.com',
  headers: {
    'Authorization': `Bearer ${getToken()}`,
  },
  timeout: 10000,
});

// 型安全なAPI呼び出し
async function fetchUserData() {
  try {
    // 単一ユーザーの取得
    const user = await apiClient.get('/users/{id}', {
      params: { id: '123' },
      schema: UserSchema,
    });
    console.log(user.name); // 型安全にアクセス可能

    // ユーザー一覧の取得
    const users = await apiClient.get('/users', {
      query: { page: 1, limit: 20 },
      schema: UsersListSchema,
    });
    users.forEach(u => console.log(u.email)); // 型安全

    // ユーザーの作成
    const newUser = await apiClient.post('/users', {
      name: '山田太郎',
      email: 'yamada@example.com',
    }, {
      schema: UserSchema,
    });

  } catch (error) {
    if (error instanceof ApiError) {
      console.error(`API Error [${error.code}]: ${error.message}`);
      if (error.status === 422) {
        console.error('Validation errors:', error.details);
      }
    }
  }
}

エラーハンドリングの型定義

Api クライアントにおけるエラーハンドリングも型安全に実装することが重要です。

エラー型の階層構造

// 基底エラークラス
abstract class BaseError extends Error {
  abstract readonly code: string;
  abstract readonly statusCode: number;
  
  constructor(message: string) {
    super(message);
    this.name = this.constructor.name;
  }
}

// ネットワークエラー
class NetworkError extends BaseError {
  readonly code = 'NETWORK_ERROR';
  readonly statusCode = 0;
}

// 認証エラー
class AuthenticationError extends BaseError {
  readonly code = 'AUTHENTICATION_ERROR';
  readonly statusCode = 401;
}

// 認可エラー
class AuthorizationError extends BaseError {
  readonly code = 'AUTHORIZATION_ERROR';
  readonly statusCode = 403;
}

// バリデーションエラー
class ValidationError extends BaseError {
  readonly code = 'VALIDATION_ERROR';
  readonly statusCode = 422;
  
  constructor(
    message: string,
    public readonly errors: Array<{
      field: string;
      message: string;
      code?: string;
    }>
  ) {
    super(message);
  }
}

// サーバーエラー
class ServerError extends BaseError {
  readonly code = 'SERVER_ERROR';
  readonly statusCode = 500;
}

Result型パターンの実装

エラーは例外ではなく、正常なプログラムフローの一部として扱うべきである。

エラーハンドリングの格言 プログラミングの知恵
// Result型の定義
type Success<T> = {
  success: true;
  data: T;
};

type Failure<E = Error> = {
  success: false;
  error: E;
};

type Result<T, E = Error> = Success<T> | Failure<E>;

// Result型を返すAPIクライアント
class SafeApiClient extends TypeSafeApiClient {
  async safeCall<P extends keyof paths, M extends keyof paths[P]>(
    path: P,
    method: M,
    options?: Parameters<TypeSafeApiClient['call']>[2]
  ): Promise<Result<ResponseBody<P, M>, BaseError>> {
    try {
      const data = await this.call(path, method, options);
      return { success: true, data };
    } catch (error) {
      if (error instanceof BaseError) {
        return { success: false, error };
      }
      
      // 未知のエラーの場合
      return {
        success: false,
        error: new ServerError(
          error instanceof Error ? error.message : '不明なエラーが発生しました'
        ),
      };
    }
  }
}

// 使用例
async function handleUserUpdate(userId: string, updates: Partial<User>) {
  const client = new SafeApiClient({ baseUrl: 'https://api.example.com' });
  
  const result = await client.safeCall('/users/{id}', 'put', {
    params: { id: userId },
    body: updates,
  });
  
  if (result.success) {
    console.log('更新成功:', result.data);
    return result.data;
  } else {
    // エラーの型に基づいた処理
    switch (result.error.code) {
      case 'VALIDATION_ERROR':
        if (result.error instanceof ValidationError) {
          console.error('バリデーションエラー:', result.error.errors);
        }
        break;
      case 'AUTHENTICATION_ERROR':
        // 再ログイン処理
        redirectToLogin();
        break;
      case 'NETWORK_ERROR':
        // リトライ処理
        console.error('ネットワークエラー。再試行してください。');
        break;
      default:
        console.error('エラー:', result.error.message);
    }
    return null;
  }
}

非同期エラーハンドリングのベストプラクティス

// 従来のtry-catch方式
class TraditionalApiService {
  async getUser(id: string): Promise<User | null> {
    try {
      const response = await fetch(`/api/users/${id}`);
      
      if (!response.ok) {
        if (response.status === 404) {
          return null;
        }
        throw new Error(`HTTP ${response.status}`);
      }
      
      const data = await response.json();
      return UserSchema.parse(data);
    } catch (error) {
      if (error instanceof z.ZodError) {
        console.error('データ形式エラー:', error);
        throw new ValidationError('不正なデータ形式', []);
      }
      
      if (error instanceof Error) {
        console.error('API呼び出しエラー:', error);
        throw error;
      }
      
      throw new Error('不明なエラー');
    }
  }
}
// Result型を使った方式
class ResultApiService {
  async getUser(id: string): Promise<Result<User | null, BaseError>> {
    try {
      const response = await fetch(`/api/users/${id}`);
      
      if (response.status === 404) {
        return { success: true, data: null };
      }
      
      if (!response.ok) {
        return {
          success: false,
          error: new ServerError(`HTTP ${response.status}`)
        };
      }
      
      const data = await response.json();
      const validated = UserSchema.safeParse(data);
      
      if (!validated.success) {
        return {
          success: false,
          error: new ValidationError(
            '不正なデータ形式',
            validated.error.errors.map(e => ({
              field: e.path.join('.'),
              message: e.message
            }))
          )
        };
      }
      
      return { success: true, data: validated.data };
    } catch (error) {
      return {
        success: false,
        error: new NetworkError(
          error instanceof Error ? error.message : 'ネットワークエラー'
        )
      };
    }
  }
}
// Either型を使った関数型アプローチ
type Left<E> = { _tag: 'Left'; left: E };
type Right<A> = { _tag: 'Right'; right: A };
type Either<E, A> = Left<E> | Right<A>;

const left = <E>(e: E): Either<E, never> => ({ _tag: 'Left', left: e });
const right = <A>(a: A): Either<never, A> => ({ _tag: 'Right', right: a });

const isLeft = <E, A>(e: Either<E, A>): e is Left<E> => e._tag === 'Left';
const isRight = <E, A>(e: Either<E, A>): e is Right<A> => e._tag === 'Right';

class EitherApiService {
  async getUser(id: string): Promise<Either<BaseError, User | null>> {
    try {
      const response = await fetch(`/api/users/${id}`);
      
      if (response.status === 404) {
        return right(null);
      }
      
      if (!response.ok) {
        return left(new ServerError(`HTTP ${response.status}`));
      }
      
      const data = await response.json();
      const validated = UserSchema.safeParse(data);
      
      if (!validated.success) {
        return left(new ValidationError('不正なデータ形式', []));
      }
      
      return right(validated.data);
    } catch (error) {
      return left(new NetworkError('ネットワークエラー'));
    }
  }
  
  // Either型のチェーン処理
  async getUserWithPosts(id: string) {
    const userResult = await this.getUser(id);
    
    if (isLeft(userResult)) {
      return userResult;
    }
    
    if (userResult.right === null) {
      return left(new NotFoundError('ユーザーが見つかりません'));
    }
    
    const postsResult = await this.getUserPosts(id);
    
    if (isLeft(postsResult)) {
      return postsResult;
    }
    
    return right({
      user: userResult.right,
      posts: postsResult.right
    });
  }
}

まとめ

Typescript の高度な型機能を活用することで、api クライアントの実装において コンパイル時とランタイムの両方で型安全性を確保できます。

本記事のポイント

  • Conditional TypesTemplate Literal Typesで柔軟な型定義が可能
  • zodによるランタイム検証で api レスポンスの安全性を確保
  • OpenAPI仕様からの型自動生成で、api とクライアントの同期を維持
  • Result型パターンでエラーハンドリングも型安全に実装
  • これらを組み合わせることで、堅牢な api クライアントを構築可能

実装チェックリスト

  • Typescript の strict モードを有効化
  • Api エンドポイントの型定義を一元管理
  • zod スキーマでランタイム検証を実装
  • エラーハンドリングの型階層を定義
  • OpenAPI 仕様からの型自動生成を導入
  • Result 型パターンでエラーを正常フローとして扱う
  • 単体テストで型推論の正確性を検証

今後の展望

型安全性の業界標準化 70 %

2025 年現在、typescript の型システムはますます強力になり、 より多くのランタイム検証ツールとの統合が進んでいます。 今後は、型定義の自動生成や ai を活用した型推論の支援など、 さらなる開発効率の向上が期待されます。

この記事は役に立ちましたか?

Daily Hackでは、開発者の皆様に役立つ情報を毎日発信しています。