はじめに
TypeScriptを学習する上で、多くの開発者が悩むtypeとinterfaceの使い分けです。この記事では、両者の違いから実際の使用場面まであらためて考えてみます。
基本的な定義と違い
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 を使用
基本原則
- オブジェクト構造 = interface:将来の拡張性と可読性を重視
- 型の組み合わせ = type:柔軟性と表現力を重視
- 一貫性を保つ:チーム内でルールを統一
- 可読性優先:初心者にも理解しやすいコードを心がける
こちらの原則に従えば、TypeScriptプロジェクトでの型定義がより整理され、保守しやすいコードベースを構築できます。参考にして適切な選択をしてください。