TypeScript開発のベストプラクティス:型安全性を高める実践的テクニック

TypeScriptを効果的に活用するためのベストプラクティスと、実際のプロジェクトで役立つ型定義のテクニックを紹介します。

TypeScript 開発のベストプラクティス

TypeScript を使った開発で、型安全性を高め、開発効率を向上させるためのベストプラクティスを解説します。

基本的な型定義

インターフェースの活用

// 良い例:インターフェースを使った型定義
interface User {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
  updatedAt: Date;
}

interface CreateUserRequest {
  name: string;
  email: string;
}

// 継承を活用した型の拡張
interface AdminUser extends User {
  role: "admin";
  permissions: string[];
}

Union Types と Type Guards

type Theme = "light" | "dark" | "auto";
type Status = "loading" | "success" | "error";

// Type Guardの実装
function isError(response: any): response is { error: string } {
  return response && typeof response.error === "string";
}

function handleApiResponse(response: unknown) {
  if (isError(response)) {
    console.error(response.error);
    return;
  }
  // 正常なレスポンスの処理
}

高度な型テクニック

ジェネリクス

// APIレスポンスの汎用型
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

// 汎用的なRepository pattern
interface Repository<T, K = number> {
  findById(id: K): Promise<T | null>;
  findAll(): Promise<T[]>;
  create(entity: Omit<T, "id">): Promise<T>;
  update(id: K, entity: Partial<T>): Promise<T>;
  delete(id: K): Promise<void>;
}

class UserRepository implements Repository<User> {
  async findById(id: number): Promise<User | null> {
    // 実装
  }

  async findAll(): Promise<User[]> {
    // 実装
  }

  // その他のメソッド...
}

Utility Types

// 既存の型から新しい型を生成
type CreateUser = Omit<User, "id" | "createdAt" | "updatedAt">;
type UpdateUser = Partial<Pick<User, "name" | "email">>;
type UserKeys = keyof User;

// Conditional Types
type NonNullable<T> = T extends null | undefined ? never : T;

// Mapped Types
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

// 使用例
type OptionalEmailUser = Optional<User, "email">;

React + TypeScript のベストプラクティス

コンポーネントの型定義

import React, { ReactNode } from "react";

interface ButtonProps {
  variant: "primary" | "secondary" | "danger";
  size?: "small" | "medium" | "large";
  disabled?: boolean;
  children: ReactNode;
  onClick?: () => void;
}

const Button: React.FC<ButtonProps> = ({
  variant,
  size = "medium",
  disabled = false,
  children,
  onClick,
}) => {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
};

// カスタムHooksの型定義
interface UseApiResult<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
  refetch: () => void;
}

function useApi<T>(url: string): UseApiResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  const fetchData = useCallback(async () => {
    try {
      setLoading(true);
      const response = await fetch(url);
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err instanceof Error ? err.message : "Unknown error");
    } finally {
      setLoading(false);
    }
  }, [url]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return { data, loading, error, refetch: fetchData };
}

設定とツール

tsconfig.json の推奨設定

{
  "compilerOptions": {
    "target": "es2020",
    "module": "esnext",
    "lib": ["dom", "dom.iterable", "es6"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "incremental": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@/components/*": ["./src/components/*"],
      "@/utils/*": ["./src/utils/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

ESLint と Prettier の設定

// .eslintrc.json
{
  "extends": [
    "@typescript-eslint/recommended",
    "@typescript-eslint/recommended-requiring-type-checking"
  ],
  "rules": {
    "@typescript-eslint/no-unused-vars": "error",
    "@typescript-eslint/explicit-function-return-type": "warn",
    "@typescript-eslint/no-explicit-any": "error"
  }
}

実践的な Tips

1. any を避ける

// 悪い例
function processData(data: any): any {
  return data.someProperty;
}

// 良い例
interface DataType {
  someProperty: string;
}

function processData(data: DataType): string {
  return data.someProperty;
}

2. 型ガードの活用

function isString(value: unknown): value is string {
  return typeof value === "string";
}

function processValue(value: unknown) {
  if (isString(value)) {
    // この時点でvalueはstring型として扱われる
    console.log(value.toUpperCase());
  }
}

3. 型アサーションは最小限に

// 避けるべき
const data = response as User;

// より安全な方法
function isUser(obj: any): obj is User {
  return obj && typeof obj.id === "number" && typeof obj.name === "string";
}

if (isUser(response)) {
  // responseはUser型として安全に使用可能
}

まとめ

TypeScript の型システムを効果的に活用することで、バグの早期発見、コードの可読性向上、開発効率の改善が期待できます。

これらのベストプラクティスを日々の開発に取り入れて、より堅牢なアプリケーションを構築しましょう。

最後まで読んでいただきありがとうございました!てばさん(@basabasa8770)でした!

この記事をシェア