TypeScript ベストプラクティス 2026 - プロの書き方

TypeScript ベストプラクティス 2026 - プロの書き方

2026年のTypeScript開発におけるベストプラクティスとパターンを学びましょう。

2026年3月16日8分で読了

はじめに:なぜTypeScriptなのか

2026年現在、TypeScriptはフロントエンド・バックエンドを問わず、JavaScript開発のスタンダードとなりました。Stack Overflowの調査によると、TypeScriptはもっとも愛されている言語の上位に常にランクインしており、新規プロジェクトの大半がTypeScriptを採用しています。

本記事では、TypeScript 5.x系での開発におけるベストプラクティスを、実践的なコード例とともに解説します。型安全性の向上、コードの保守性改善、パフォーマンス最適化など、プロフェッショナルなTypeScript開発者が知っておくべきテクニックを網羅します。

1. 厳格な型設定を有効にする

tsconfig.json の推奨設定

TypeScriptの型安全性を最大限に活用するため、strictモードを有効にしましょう。

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "exactOptionalPropertyTypes": true,
    "forceConsistentCasingInFileNames": true,
    "verbatimModuleSyntax": true,
    "moduleResolution": "bundler",
    "module": "ESNext",
    "target": "ES2022",
    "lib": ["ES2023", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "isolatedModules": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  }
}

strict: trueは以下のオプションをすべて有効にします:

  • strictNullChecks - null/undefinedの厳格なチェック
  • strictFunctionTypes - 関数型の厳格なチェック
  • strictBindCallApply - bind/call/applyの厳格な型チェック
  • strictPropertyInitialization - プロパティの初期化チェック
  • noImplicitAny - 暗黙のany型を禁止
  • noImplicitThis - 暗黙のthis型を禁止
  • alwaysStrict - 常にstrictモード

noUncheckedIndexedAccess の重要性

この設定は配列やオブジェクトへのインデックスアクセスの安全性を大幅に向上させます。

const items = ["apple", "banana", "cherry"];

// noUncheckedIndexedAccess: false の場合
const item = items[5]; // string と推論される(危険!)

// noUncheckedIndexedAccess: true の場合
const item = items[5]; // string | undefined と推論される(安全!)

// 安全なアクセスパターン
const safeItem = items[0];
if (safeItem !== undefined) {
  console.log(safeItem.toUpperCase()); // 安全に使える
}

2. 型推論を活用する

TypeScriptの型推論は非常に優秀です。冗長な型注釈を避け、型推論に任せるべき場面を知りましょう。

型注釈が不要な場面

// 悪い例:冗長な型注釈
const name: string = "太郎";
const age: number = 25;
const isActive: boolean = true;
const items: string[] = ["a", "b", "c"];

// 良い例:型推論に任せる
const name = "太郎";
const age = 25;
const isActive = true;
const items = ["a", "b", "c"];

型注釈が必要な場面

// 関数のパラメータには型注釈が必要
function greet(name: string): string {
  return `こんにちは、${name}さん`;
}

// 初期値がない変数
let userId: string;

// 複雑な型の場合
const config: Record<string, { enabled: boolean; timeout: number }> = {};

// 戻り値の型がコンテキストから明確でない場合
function parseResponse(data: unknown): UserProfile {
  // ...
}

3. ユニオン型とナローイング

判別可能なユニオン型(Discriminated Unions)

TypeScriptでもっとも強力なパターンの一つが判別可能なユニオン型です。

// 判別プロパティ 'type' を持つユニオン型
type ApiResponse =
  | { type: "success"; data: User; statusCode: 200 }
  | { type: "error"; message: string; statusCode: 400 | 500 }
  | { type: "loading" };

function handleResponse(response: ApiResponse) {
  switch (response.type) {
    case "success":
      // response.data が安全にアクセス可能
      console.log(response.data.name);
      break;
    case "error":
      // response.message が安全にアクセス可能
      console.error(response.message);
      break;
    case "loading":
      console.log("読み込み中...");
      break;
    default:
      // 網羅性チェック
      const _exhaustive: never = response;
      throw new Error(`未処理のケース: ${_exhaustive}`);
  }
}

カスタム型ガード

interface Cat {
  meow(): void;
  purr(): void;
}

interface Dog {
  bark(): void;
  fetch(): void;
}

// カスタム型ガード
function isCat(animal: Cat | Dog): animal is Cat {
  return "meow" in animal;
}

function handleAnimal(animal: Cat | Dog) {
  if (isCat(animal)) {
    animal.meow(); // Cat として認識
  } else {
    animal.bark(); // Dog として認識
  }
}

4. ジェネリクスの効果的な使用

基本的なジェネリクス

// APIレスポンスのラッパー型
interface ApiResult<T> {
  data: T;
  status: number;
  timestamp: Date;
}

// 汎用的なフェッチ関数
async function fetchApi<T>(url: string): Promise<ApiResult<T>> {
  const response = await fetch(url);
  const data = await response.json();
  return {
    data: data as T,
    status: response.status,
    timestamp: new Date(),
  };
}

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

const result = await fetchApi<User>("/api/users/1");
console.log(result.data.name); // 型安全

制約付きジェネリクス

// オブジェクトのキーを安全に取得
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "太郎", age: 25, email: "taro@example.com" };
const name = getProperty(user, "name"); // string
const age = getProperty(user, "age"); // number
// getProperty(user, "invalid"); // コンパイルエラー

// 最小限のインターフェースを要求するジェネリクス
function getDisplayName<T extends { firstName: string; lastName: string }>(
  entity: T
): string {
  return `${entity.lastName} ${entity.firstName}`;
}

条件型(Conditional Types)

// 型レベルの条件分岐
type IsString<T> = T extends string ? true : false;

type A = IsString<string>; // true
type B = IsString<number>; // false

// 実践的な例:Promiseのアンラップ
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type Result = UnwrapPromise<Promise<string>>; // string
type Plain = UnwrapPromise<number>; // number

// 関数の戻り値型を変更する
type AsyncReturnType<T extends (...args: any[]) => Promise<any>> =
  T extends (...args: any[]) => Promise<infer R> ? R : never;

5. ユーティリティ型の活用

TypeScriptの組み込みユーティリティ型を活用しましょう。

interface User {
  id: string;
  name: string;
  email: string;
  age: number;
  role: "admin" | "user" | "moderator";
  createdAt: Date;
}

// Partial - すべてのプロパティをオプショナルに
type UpdateUserInput = Partial<User>;

// Required - すべてのプロパティを必須に
type CompleteUser = Required<User>;

// Pick - 特定のプロパティのみを選択
type UserPreview = Pick<User, "id" | "name" | "role">;

// Omit - 特定のプロパティを除外
type CreateUserInput = Omit<User, "id" | "createdAt">;

// Record - キーと値の型を指定したオブジェクト型
type UserRolePermissions = Record<User["role"], string[]>;

// Readonly - すべてのプロパティを読み取り専用に
type ImmutableUser = Readonly<User>;

// 組み合わせて使う
type UserUpdatePayload = Partial<Omit<User, "id" | "createdAt">>;

カスタムユーティリティ型の作成

// DeepPartial - ネストされたオブジェクトもPartialに
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// DeepReadonly - ネストされたオブジェクトもReadonlyに
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

// NonNullableProperties - すべてのプロパティからnull/undefinedを除去
type NonNullableProperties<T> = {
  [P in keyof T]: NonNullable<T[P]>;
};

// RequireAtLeastOne - 少なくとも1つのプロパティが必須
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<
  T,
  Exclude<keyof T, Keys>
> &
  {
    [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>;
  }[Keys];

6. エラーハンドリングのベストプラクティス

Result型パターン

例外を投げる代わりに、Result型を使用して型安全なエラーハンドリングを実現しましょう。

type Result<T, E = Error> =
  | { success: true; data: T }
  | { success: false; error: E };

// 使用例
async function fetchUser(id: string): Promise<Result<User, string>> {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      return { success: false, error: `HTTP Error: ${response.status}` };
    }
    const data = await response.json();
    return { success: true, data };
  } catch (e) {
    return {
      success: false,
      error: e instanceof Error ? e.message : "不明なエラー",
    };
  }
}

// 呼び出し側
const result = await fetchUser("123");
if (result.success) {
  console.log(result.data.name); // 型安全にアクセス
} else {
  console.error(result.error); // エラーメッセージにアクセス
}

カスタムエラークラス

class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number,
    public readonly details?: Record<string, unknown>
  ) {
    super(message);
    this.name = "AppError";
  }
}

class ValidationError extends AppError {
  constructor(
    message: string,
    public readonly fields: Record<string, string[]>
  ) {
    super(message, "VALIDATION_ERROR", 400, { fields });
    this.name = "ValidationError";
  }
}

class NotFoundError extends AppError {
  constructor(resource: string, id: string) {
    super(
      `${resource}(ID: ${id})が見つかりません`,
      "NOT_FOUND",
      404
    );
    this.name = "NotFoundError";
  }
}

7. 列挙型(Enum)の代替案

TypeScriptのenumにはいくつかの問題があります。代わりにas constを使用するパターンが推奨されます。

// 従来のenum(非推奨の場合が多い)
enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

// 推奨: as const を使用
const DIRECTION = {
  Up: "UP",
  Down: "DOWN",
  Left: "LEFT",
  Right: "RIGHT",
} as const;

type Direction = (typeof DIRECTION)[keyof typeof DIRECTION];
// "UP" | "DOWN" | "LEFT" | "RIGHT"

// HTTPステータスコードの例
const HTTP_STATUS = {
  OK: 200,
  Created: 201,
  BadRequest: 400,
  Unauthorized: 401,
  Forbidden: 403,
  NotFound: 404,
  InternalServerError: 500,
} as const;

type HttpStatus = (typeof HTTP_STATUS)[keyof typeof HTTP_STATUS];

8. 型安全なイベントハンドリング

// イベントマップの定義
interface EventMap {
  "user:login": { userId: string; timestamp: Date };
  "user:logout": { userId: string };
  "item:added": { itemId: string; quantity: number };
  "error": { message: string; code: number };
}

// 型安全なイベントエミッター
class TypedEventEmitter<T extends Record<string, any>> {
  private listeners = new Map<string, Set<Function>>();

  on<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
    if (!this.listeners.has(event as string)) {
      this.listeners.set(event as string, new Set());
    }
    this.listeners.get(event as string)!.add(listener);
  }

  emit<K extends keyof T>(event: K, data: T[K]): void {
    const eventListeners = this.listeners.get(event as string);
    if (eventListeners) {
      eventListeners.forEach((listener) => listener(data));
    }
  }

  off<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
    this.listeners.get(event as string)?.delete(listener);
  }
}

// 使用例
const emitter = new TypedEventEmitter<EventMap>();

emitter.on("user:login", (data) => {
  console.log(data.userId); // 型安全
  console.log(data.timestamp); // 型安全
});

emitter.emit("user:login", {
  userId: "123",
  timestamp: new Date(),
}); // 型チェックされる

9. テンプレートリテラル型

TypeScript 4.1以降で使用できるテンプレートリテラル型は、文字列ベースの型を強力にサポートします。

// APIエンドポイントの型安全な定義
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
type ApiVersion = "v1" | "v2";
type Resource = "users" | "posts" | "comments";

type ApiEndpoint = `/${ApiVersion}/${Resource}`;
// "/v1/users" | "/v1/posts" | "/v1/comments" | "/v2/users" | ...

// CSSプロパティの型
type CSSUnit = "px" | "em" | "rem" | "%" | "vh" | "vw";
type CSSValue = `${number}${CSSUnit}`;

const width: CSSValue = "100px"; // OK
const height: CSSValue = "50vh"; // OK
// const invalid: CSSValue = "abc"; // エラー

// イベント名の生成
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
type ChangeEvent = EventName<"change">; // "onChange"

10. パフォーマンスに関する注意点

型の複雑さを制御する

// 悪い例:過度に複雑な型(コンパイル時間に影響)
type DeepNested<T, Depth extends number[]> = Depth["length"] extends 10
  ? T
  : { value: T; nested: DeepNested<T, [...Depth, 0]> };

// 良い例:適度な複雑さに留める
type MaxDepth3<T> = {
  value: T;
  nested?: {
    value: T;
    nested?: {
      value: T;
    };
  };
};

プロジェクト参照による分割ビルド

大規模プロジェクトでは、プロジェクト参照を使用してビルドを分割しましょう。

{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true
  },
  "references": [
    { "path": "./packages/shared" },
    { "path": "./packages/api" },
    { "path": "./packages/web" }
  ]
}

まとめ

TypeScriptのベストプラクティスは、単に型を付けるだけでなく、型システムを最大限に活用してコードの安全性と保守性を向上させることです。

本記事で紹介した主要なポイント:

  1. 厳格な型設定を有効にして安全性を最大化する
  2. 型推論を活用して冗長な型注釈を避ける
  3. 判別可能なユニオン型でパターンマッチングを活用する
  4. ジェネリクスで再利用可能な型を作成する
  5. ユーティリティ型を組み合わせて効率的に型を定義する
  6. Result型パターンで型安全なエラーハンドリングを実現する
  7. as constでenumの代替として使用する
  8. テンプレートリテラル型で文字列パターンを型レベルで保証する

TypeScriptの型システムは日々進化しています。公式ドキュメントやリリースノートを定期的にチェックして、最新の機能を活用しましょう。

開発中のJSON確認にはJSONフォーマッターを、正規表現のデバッグには正規表現テスターをご活用ください。また、Web開発の最新動向については2026年のWeb開発トレンドもご参照ください。

関連記事