3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

typeとinterfaceの使い分けについて考える

3
Posted at

はじめに

TypeScriptを学習する上で、多くの開発者が悩むtypeinterfaceの使い分けです。この記事では、両者の違いから実際の使用場面まであらためて考えてみます。

基本的な定義と違い

interfaceとは

オブジェクトの「契約」や「設計図」を定義するための仕組みです。クラスやオブジェクトがどのような構造を持つべきかを示します。

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

typeとは

型に別名をつけたり、複雑な型を組み合わせたりするための仕組みです。より柔軟で汎用的な型定義ができます。

type User = {
  id: number;
  name: string;
  email: string;
}

詳細比較表

特徴 interface type 初心者へのポイント
基本用途 オブジェクト構造の定義 あらゆる型の別名定義 オブジェクト → interface、その他 → type
拡張性 extendsで継承可能 &で交差型として組み合わせ interfaceの方が直感的
宣言マージ ◯ 同名で複数回宣言可能 × エラーになる ライブラリ拡張時に便利
ユニオン型 × 直接定義不可 ◯ パイプ記号で簡単に定義 複数の型から1つ選ぶ場合はtype
プリミティブ型 × オブジェクト型のみ ◯ すべての型に対応 文字列や数値の別名はtype
条件付き型 × 使用不可 ◯ 使用可能 高度な型操作はtype
クラス実装 implementsで使用 ◯ オブジェクト型なら使用可 どちらでもOK
計算量 軽微に高速 軽微に低速 実用上は差なし

コード例で学ぶ使い分け

1. 基本的なオブジェクト定義

【推奨】interface

// ユーザー情報のような基本的なオブジェクト
interface User {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

// 将来的に拡張可能
interface AdminUser extends User {
  permissions: string[];
  role: 'admin' | 'super-admin';
}

【使用可能だが非推奨】type

// 動作するが、interfaceの方が適切
type User = {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

2. ユニオン型やプリミティブ型

【推奨】type

// 文字列リテラル型のユニオン
type Status = 'loading' | 'success' | 'error';

// プリミティブ型の別名
type ID = string | number;

// 複雑なユニオン型
type ApiResponse = 
  | { status: 'success'; data: User[] }
  | { status: 'error'; message: string };

【不可能】interface

// これはエラーになる
interface Status = 'loading' | 'success' | 'error'; // エラー

3. 関数の型定義

【推奨】type

// 関数の型定義
type EventHandler = (event: Event) => void;
type ApiCall<T> = (data: T) => Promise<ApiResponse>;

// 複雑な関数型
type Middleware = (
  req: Request,
  res: Response,
  next: NextFunction
) => void;

【使用可能】interface

// 関数型も定義可能だが、typeの方が読みやすい
interface EventHandler {
  (event: Event): void;
}

4. 宣言マージの活用

interface の強み

// ライブラリの型を拡張(例:Express.js)
interface Request {
  user?: User; // 最初の宣言
}

interface Request {
  sessionId?: string; // 自動的にマージされる
}

// 結果的に以下と同じ
interface Request {
  user?: User;
  sessionId?: string;
}

5. 条件付き型と高度な型操作

type の強み

// 条件付き型
type NonNullable<T> = T extends null | undefined ? never : T;

// マップド型
type Partial<T> = {
  [P in keyof T]?: T[P];
}

// テンプレートリテラル型
type EventName<T extends string> = `on${Capitalize<T>}`;

// これらはinterfaceでは不可能

実践的なシナリオ別使い分け

シナリオ1: APIレスポンス定義

// 基本的なデータ構造 → interface
interface Product {
  id: string;
  name: string;
  price: number;
  category: Category;
}

interface Category {
  id: string;
  name: string;
}

// APIレスポンスの種類 → type
type ApiResponse<T> = 
  | { success: true; data: T }
  | { success: false; error: string };

// 使用例
type ProductResponse = ApiResponse<Product[]>;

シナリオ2: Reactコンポーネントの Props

// 基本的なProps → interface
interface ButtonProps {
  children: React.ReactNode;
  onClick: () => void;
  disabled?: boolean;
  variant?: 'primary' | 'secondary';
}

// 条件付きProps → type
type InputProps = {
  value: string;
  onChange: (value: string) => void;
} & (
  | { type: 'text'; maxLength?: number }
  | { type: 'number'; min?: number; max?: number }
);

シナリオ3: 設定オブジェクト

// 基本設定 → interface
interface DatabaseConfig {
  host: string;
  port: number;
  database: string;
  ssl?: boolean;
}

// 環境別設定 → type
type Environment = 'development' | 'staging' | 'production';

type Config = {
  database: DatabaseConfig;
  environment: Environment;
  features: Record<string, boolean>;
};

Next.jsでの実例

ページコンポーネントの型定義

// pages/user/[id].tsx
import { GetServerSideProps, NextPage } from 'next';

// データ構造 → interface
interface User {
  id: string;
  name: string;
  email: string;
  profile: UserProfile;
}

interface UserProfile {
  bio?: string;
  avatar?: string;
  socialLinks: SocialLink[];
}

interface SocialLink {
  platform: string;
  url: string;
}

// Propsの型 → interface(拡張可能性があるため)
interface UserPageProps {
  user: User;
  isOwner: boolean;
}

// NextPageの型 → type(組み合わせ型のため)
type UserPage = NextPage<UserPageProps>;

const UserPage: UserPage = ({ user, isOwner }) => {
  return (
    <div>
      <h1>{user.name}</h1>
      {/* コンポーネント内容 */}
    </div>
  );
};

export default UserPage;

// SSRの戻り値型 → type
type ServerSideProps = {
  props: UserPageProps;
} | {
  notFound: true;
};

export const getServerSideProps: GetServerSideProps<UserPageProps> = async (context) => {
  // データ取得ロジック
  return {
    props: {
      user,
      isOwner
    }
  };
};

API Routes の型定義

// pages/api/users/[id].ts
import { NextApiRequest, NextApiResponse } from 'next';

// APIレスポンス → type(ユニオン型のため)
type ApiResponse = 
  | { success: true; user: User }
  | { success: false; error: string };

// リクエストパラメータ → interface
interface UserApiRequest extends NextApiRequest {
  query: {
    id: string;
  };
  body?: Partial<User>;
}

export default async function handler(
  req: UserApiRequest,
  res: NextApiResponse<ApiResponse>
) {
  if (req.method === 'GET') {
    // GET ロジック
  } else if (req.method === 'PUT') {
    // PUT ロジック
  }
}

よくある間違いと注意点

【間違い1】すべてtypeで定義

// 悪い例:オブジェクト構造もtypeで定義
type User = {
  id: number;
  name: string;
}

type AdminUser = User & {
  permissions: string[];
}
// 良い例:オブジェクト構造はinterfaceで
interface User {
  id: number;
  name: string;
}

interface AdminUser extends User {
  permissions: string[];
}

【間違い2】ユニオン型にinterfaceを使用

// 悪い例:これはエラーになる
interface Status = 'loading' | 'success' | 'error'; // エラー
// 良い例:ユニオン型はtypeで
type Status = 'loading' | 'success' | 'error';

【間違い3】一貫性のない命名

// 悪い例:混在していて分かりにくい
interface UserType {
  id: number;
}

type UserInterface = {
  name: string;
}
// 良い例:用途に応じた適切な命名
interface User {
  id: number;
  name: string;
}

type UserStatus = 'active' | 'inactive' | 'pending';

パフォーマンスと最適化

TypeScriptコンパイラでの違い

// interface: わずかに高速(型チェック時)
interface FastInterface {
  id: number;
  name: string;
}

// type: わずかに低速(型解決時)
type SlowType = {
  id: number;
  name: string;
}

実際の影響

  • 小規模プロジェクト:体感できる差はなし
  • 大規模プロジェクト:数秒の差が出る場合がある
  • 推奨:パフォーマンスよりも可読性と保守性を優先

最適化のベストプラクティス

// 良い例:適材適所
interface BaseEntity {
  id: string;
  createdAt: Date;
  updatedAt: Date;
}

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

type UserWithPosts = User & {
  posts: Post[];
};

type LoadingState = 'idle' | 'loading' | 'success' | 'error';

まとめ

使い分けの決定フロー

オブジェクトの構造を定義?
├─ Yes → interface を使用
└─ No
   ├─ ユニオン型? → type を使用
   ├─ プリミティブ型の別名? → type を使用
   ├─ 関数型? → type を使用
   └─ 条件付き型・高度な型操作? → type を使用

基本原則

  1. オブジェクト構造 = interface:将来の拡張性と可読性を重視
  2. 型の組み合わせ = type:柔軟性と表現力を重視
  3. 一貫性を保つ:チーム内でルールを統一
  4. 可読性優先:初心者にも理解しやすいコードを心がける

こちらの原則に従えば、TypeScriptプロジェクトでの型定義がより整理され、保守しやすいコードベースを構築できます。参考にして適切な選択をしてください。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?