TypeScript 速習チュートリアル

TypeScript ベストプラクティス

このガイドでは、クリーンでメンテナンス性が高く、タイプセーフなコードを書くための TypeScript 必須ベストプラクティスを網羅しています。これらのプラクティスに従うことで、コードの品質と開発者のエクスペリエンス(DX)が大幅に向上します。

1. プロジェクト設定

1.1 Strict モードの有効化

最高のタイプセーフティを確保するために、tsconfig.json では常に strict モードを有効にしてください。

tsconfig.json

{
  "compilerOptions": {
    /* すべての厳密な型チェックオプションを有効にする */
    "strict": true,
    /* その他の推奨設定 */
    "target": "ES2020",
    "module": "commonjs",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

1.2 追加の厳密なチェック

さらにコードの品質を高めるために、以下の追加チェックの有効化を検討してください。

{
  "compilerOptions": {
    /* 追加の厳密なチェック */
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true
  }
}

2. 型システムのベストプラクティス

2.1 可能な限り型推論を活用する

代入から型が明らかな場合は、TypeScript に型を推論(Inference)させましょう。

コード例

// Bad: 冗長な型アノテーション
const name: string = 'John';

// Good: TypeScript に型を推論させる
const name = 'John';

// Bad: 冗長な戻り値の型定義
function add(a: number, b: number): number {
  return a + b;
}

// Good: TypeScript に戻り値の型を推論させる
function add(a: number, b: number) {
  return a + b;
}

2.2 精密な型アノテーション

パブリック API や関数のパラメータ(引数)には、明示的に型を定義しましょう。

コード例

// Bad: 型情報がない
function processUser(user) {
  return user.name.toUpperCase();
}

// Good: 明示的なパラメータと戻り値の型
interface User {
  id: number;
  name: string;
  email?: string; // オプショナルなプロパティ
}

function processUser(user: User): string {
  return user.name.toUpperCase();
}

2.3 インターフェース vs 型エイリアス

interfacetype の使い分けを理解しましょう。

コード例

// 拡張(extend)や実装(implement)が可能なオブジェクトの形状には interface を使用
interface User {
  id: number;
  name: string;
}

// インターフェースの拡張
interface AdminUser extends User {
  permissions: string[];
}

// ユニオン、タプル、またはマップ型には type を使用
type UserRole = 'admin' | 'editor' | 'viewer';

// ユニオン型
type UserId = number | string;

// マップ型(Mapped Types)
type ReadonlyUser = Readonly<User>;

// タプル型
type Point = [number, number];

2.4 any 型を避ける

any よりも、より具体的な型を優先してください。

コード例

// Bad: 型安全性が失われる
function logValue(value: any) {
  console.log(value.toUpperCase()); // 実行時までエラーに気付かない
}

// Better: ジェネリックな型パラメータを使用
function logValue<T>(value: T) {
  console.log(String(value)); // より安全だが、まだ理想的ではない
}

// Best: 期待される型を具体的に指定
function logString(value: string) {
  console.log(value.toUpperCase()); // タイプセーフ
}

// どんな値でも受け付ける必要があるが、タイプセーフを維持したい場合
function logUnknown(value: unknown) {
  if (typeof value === 'string') {
    console.log(value.toUpperCase());
  } else {
    console.log(String(value));
  }
}

3. コードの組織化

3.1 モジュールの構成

明確な責任を持つ論理的なモジュールにコードを整理します。

コード例

// user/user.model.ts
export interface User {
  id: string;
  name: string;
  email: string;
}

// user/user.service.ts
import { User } from './user.model';

export class UserService {
  private users: User[] = [];

  addUser(user: User) {
    this.users.push(user);
  }

  getUser(id: string): User | undefined {
    return this.users.find(user => user.id === id);
  }
}

// user/index.ts (バレルファイル)
export * from './user.model';
export * from './user.service';

3.2 ファイル命名規則

一貫したファイル命名パターンに従ってください。

コード例

// Good
user.service.ts   // サービスクラス
user.model.ts     // 型定義
user.controller.ts // コントローラー
user.component.ts  // コンポーネント
user.utils.ts      // ユーティリティ関数
user.test.ts       // テストファイル

// Bad
UserService.ts     // ファイル名に PascalCase を避ける
user_service.ts    // snake_case を避ける
userService.ts     // ファイル名に camelCase を避ける

4. ベストプラクティスの原則

  • 型(Types)とインターフェース(Interfaces)をドキュメント化する。
  • 型の設計においては、継承(Inheritance)よりも合成(Composition)を優先する。
  • tsconfig.json を厳格に保ち、最新の状態に更新する。
  • コードベースの進化に合わせて、より具体的な型を使用するようにリファクタリングする。

5. 関数とメソッド

5.1 関数のパラメータと戻り値の型

適切なパラメータと戻り値の型を設定し、明確でタイプセーフな関数を書きます。

コード例

// Bad: 型情報がない
function process(user, notify) {
  notify(user.name);
}

// Good: 明示的なパラメータと戻り値の型
function processUser(
  user: User,
  notify: (message: string) => void
): void {
  notify(`ユーザーを処理中: ${user.name}`);
}

// 条件分岐の代わりにデフォルト引数を使用
function createUser(
  name: string,
  role: UserRole = 'viewer',
  isActive: boolean = true
): User {
  return { name, role, isActive };
}

// 可変引数にはレストパラメータ(残余引数)を使用
function sum(...numbers: number[]): number {
  return numbers.reduce((total, num) => total + num, 0);
}

5.2 関数の過剰使用を避ける

関数の複雑さと責任(責務)に注意を払ってください。

コード例

// Bad: 責任が多すぎる
function processUserData(userData: any) {
  // バリデーション
  if (!userData || !userData.name) throw new Error('無効なユーザーデータ');

  // データ変換
  const processedData = {
    ...userData,
    name: userData.name.trim(),
    createdAt: new Date()
  };

  // サイドエフェクト(副作用)
  saveToDatabase(processedData);

  // 通知
  sendNotification(processedData.email, 'プロファイルが更新されました');

  return processedData;
}

// Better: 小さく、焦点の絞られた関数に分割する
function validateUserData(data: unknown): UserData {
  if (!data || typeof data !== 'object') {
    throw new Error('無効なユーザーデータ');
  }
  return data as UserData;
}

function processUserData(userData: UserData): ProcessedUserData {
  return {
    ...userData,
    name: userData.name.trim(),
    createdAt: new Date()
  };
}

6. 非同期処理のパターン

6.1 適切な Async/Await の使用

非同期操作は、適切なエラーハンドリングを伴って効果的に処理します。

コード例

// Bad: エラーを処理していない
async function fetchData() {
  const response = await fetch('/api/data');
  return response.json();
}

// Good: 適切なエラーハンドリング
async function fetchData<T>(url: string): Promise<T> {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTPエラー! ステータス: ${response.status}`);
    }
    return await response.json() as T;
  } catch (error) {
    console.error('データの取得に失敗しました:', error);
    throw error; // 呼び出し元で処理できるように再スロー
  }
}

// Better: 並列操作には Promise.all を使用
async function fetchMultipleData<T>(urls: string[]): Promise<T[]> {
  try {
    const promises = urls.map(url => fetchData<T>(url));
    return await Promise.all(promises);
  } catch (error) {
    console.error('一つ以上のリクエストが失敗しました:', error);
    throw error;
  }
}

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

// 適切な型付けでユーザーデータを取得
async function getUserData(userId: string): Promise<User> {
  return fetchData<User>(`/api/users/${userId}`);
}

6.2 非同期処理のネストを避ける

コールバック地獄を避けるために、async/await コードをフラット化します。

コード例

// Bad: ネストされた async/await (コールバック地獄)
async function processUser(userId: string) {
  const user = await getUser(userId);
  if (user) {
    const orders = await getOrders(user.id);
    if (orders.length > 0) {
      const latestOrder = orders[0];
      const items = await getOrderItems(latestOrder.id);
      return { user, latestOrder, items };
    }
  }
  return null;
}

// Better: async/await チェーンのフラット化
async function processUser(userId: string) {
  const user = await getUser(userId);
  if (!user) return null;

  const orders = await getOrders(user.id);
  if (orders.length === 0) return { user, latestOrder: null, items: [] };

  const latestOrder = orders[0];
  const items = await getOrderItems(latestOrder.id);

  return { user, latestOrder, items };
}

// Best: 独立した非同期操作には Promise.all を使用
async function processUser(userId: string) {
  const [user, orders] = await Promise.all([
    getUser(userId),
    getOrders(userId)
  ]);

  if (!user) return null;
  if (orders.length === 0) return { user, latestOrder: null, items: [] };

  const latestOrder = orders[0];
  const items = await getOrderItems(latestOrder.id);

  return { user, latestOrder, items };
}

7. テストと品質

7.1 テスト可能なコードを書く

依存性の注入(Dependency Injection)や純粋関数(Pure Functions)を使用して、テストのしやすさを考慮した設計にします。

コード例

// Bad: 直接的な依存関係によりテストが困難
class PaymentProcessor {
  async processPayment(amount: number) {
    const paymentGateway = new PaymentGateway();
    return paymentGateway.charge(amount);
  }
}

// Better: 依存性の注入を使用
interface PaymentGateway {
  charge(amount: number): Promise<boolean>;
}

class PaymentProcessor {
  constructor(private paymentGateway: PaymentGateway) {}

  async processPayment(amount: number): Promise<boolean> {
    if (amount <= 0) {
      throw new Error('金額は0より大きい必要があります');
    }
    return this.paymentGateway.charge(amount);
  }
}

// Jest を使用したテスト例
describe('PaymentProcessor', () => {
  let processor: PaymentProcessor;
  let mockGateway: jest.Mocked<PaymentGateway>;

  beforeEach(() => {
    mockGateway = {
      charge: jest.fn()
    };
    processor = new PaymentProcessor(mockGateway);
  });

  it('有効な支払いを処理できること', async () => {
    mockGateway.charge.mockResolvedValue(true);
    const result = await processor.processPayment(100);
    expect(result).toBe(true);
    expect(mockGateway.charge).toHaveBeenCalledWith(100);
  });

  it('無効な金額に対してスローすること', async () => {
    await expect(processor.processPayment(-50))
      .rejects
      .toThrow('金額は0より大きい必要があります');
  });
});

7.2 型のテスト

型アサーションやユーティリティを使用して、型が期待通りに動作することを確認します。

コード例

// @ts-expect-error を使用して型エラーをテスト
// @ts-expect-error - 負の値は許可されないはず
const invalidUser: User = { id: -1, name: 'テスト' };

// テスト内での型アサーションの使用
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error('文字列ではありません');
  }
}

// テスト用のユーティリティ型
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false

// tsd を使用した型のテスト (インストール: npm install --save-dev tsd)
/*
import { expectType } from 'tsd';

const user = { id: 1, name: 'John' };
expectType<{ id: number; name: string }>(user);
expectType<string>(user.name);
*/

8. パフォーマンスに関する考慮事項

8.1 型のみのインポートとエクスポート

バンドルサイズの削減とツリーシェイキングを向上させるために、型のみのインポート/エクスポートを使用します。

コード例

// Bad: 型と値の両方をインポート
import { User, fetchUser } from './api';

// Good: 型と値のインポートを分ける
import type { User } from './api';
import { fetchUser } from './api';

// さらに良い: 可能な限り型のみのインポートを使用
import type { User, UserSettings } from './types';

// 型のみのエクスポート
export type { User };

// 実行時のエクスポート
export { fetchUser };

// tsconfig.json で "isolatedModules": true を有効にすると
// 型のみのインポートが適切に処理されることを保証できます

8.2 過度な型の複雑さを避ける

コンパイル時間に影響を与える可能性のある複雑な型に注意してください。

コード例

// Bad: 深くネストされたマップ型は遅くなる可能性がある
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// Better: 可能な限り組み込みのユーティリティ型を使用
type User = {
  id: string;
  profile: {
    name: string;
    email: string;
  };
  preferences?: {
    notifications: boolean;
  };
};

// DeepPartial<User> の代わりに、型アサーションを伴う Partial を使用
const updateUser = (updates: Partial<User>) => {
  // 実装
};

// 複雑な型にはインターフェースの使用を検討
interface UserProfile {
  name: string;
  email: string;
}

interface UserPreferences {
  notifications: boolean;
}

interface User {
  id: string;
  profile: UserProfile;
  preferences?: UserPreferences;
}

8.3 リテラル型には const アサーションを使用

as const を使用して型推論とパフォーマンスを向上させます。

コード例

// const アサーションなし (広い型)
const colors = ['red', 'green', 'blue'];
// 型: string[]

// const アサーションあり (より狭く精密な型)
const colors = ['red', 'green', 'blue'] as const;
// 型: readonly ["red", "green", "blue"]

// const 配列からユニオン型を抽出
type Color = typeof colors[number]; // "red" | "green" | "blue"

// const アサーションを伴うオブジェクト
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  features: ['auth', 'notifications'],
} as const;

// 型は以下のようになります:
// {
//   readonly apiUrl: "https://api.example.com";
//   readonly timeout: 5000;
//   readonly features: readonly ["auth", "notifications"];
// }

9. 避けるべき一般的な間違い

9.1 any 型の多用

any を使用すると TypeScript の型チェックが無効化されてしまうため、避けましょう。

コード例

// Bad: すべての型安全性が失われる
function process(data: any) {
  return data.map(item => item.name);
}

// Better: 型安全性のためにジェネリクスを使用
function process<T extends { name: string }>(items: T[]) {
  return items.map(item => item.name);
}

// Best: 可能な限り具体的な型を使用
interface User {
  name: string;
  age: number;
}

function processUsers(users: User[]) {
  return users.map(user => user.name);
}

9.2 Strict モードを使用していない

tsconfig.json では常に strict モードを有効にしてください。

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    /* その他の厳密さフラグ */
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true
  }
}

9.3 型推論の無視

可能な箇所では TypeScript に型を推論させましょう。

コード例

// 冗長な型アノテーション
const name: string = 'John';

// 型を推論させる
const name = 'John'; // TypeScript は string だと理解します

// 冗長な戻り値の型
function add(a: number, b: number): number {
  return a + b;
}

// 戻り値の型を推論させる
function add(a: number, b: number) {
  return a + b; // TypeScript は number と推論します
}

9.4 型ガードの不使用

型を安全に絞り込むために型ガード(Type Guards)を使用しましょう。

コード例

// 型ガードなし
function process(input: string | number) {
  return input.toUpperCase(); // エラー: number に toUpperCase は存在しません
}

// 型ガードあり
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function process(input: string | number) {
  if (isString(input)) {
    return input.toUpperCase(); // ここでは input が string であると認識されます
  } else {
    return input.toFixed(2); // ここでは input が number であると認識されます
  }
}

// 組み込みの型ガード
if (typeof value === 'string') { /* string 型 */ }
if (value instanceof Date) { /* Date インスタンス */ }
if ('id' in user) { /* id プロパティが存在する */ }

9.5 null と undefined の未処理

潜在的な nullundefined の値は必ず処理してください。

コード例

// Bad: 実行時エラーの可能性
function getLength(str: string | null) {
  return str.length; // エラー: オブジェクトは 'null' の可能性があります
}

// Good: Null チェック
function getLength(str: string | null) {
  if (str === null) return 0;
  return str.length;
}

// Better: オプショナルチェイニングと Null 合体演算子を使用
function getLength(str: string | null) {
  return str?.length ?? 0;
}

// 配列の場合
const names: string[] | undefined = [];
const count = names?.length ?? 0; // undefined を安全に処理

// オブジェクトのプロパティ
interface User {
  profile?: {
    name?: string;
  };
}

const user: User = {};
const name = user.profile?.name ?? 'Anonymous';