Chapter 03

インターフェースと型エイリアス

オブジェクトの形を定義する interface と type の使い方を学びます

30 min

このチャプターで学ぶこと

  • interface でオブジェクトの形を定義できる
  • type エイリアスの使い方を理解できる
  • optional プロパティと readonly を使える
  • interface の extends と intersection 型を理解できる
  • interface と type の使い分けを判断できる

インターフェースと型エイリアス

前のチャプターで stringnumber といったプリミティブ型を学びました。しかし実際のアプリケーションでは、ユーザー情報や商品データなど**オブジェクトの形(構造)**を定義することがほとんどです。

このチャプターでは、オブジェクトの「形」を定義する2つの方法 ── interfacetype ── を学びます。

なぜオブジェクトの「形」を定義するのか

LaravelでFormRequestを書くとき、バリデーションルールで「このリクエストにはどんなフィールドが必要か」を宣言しますよね。

// Laravel の FormRequest(参考)
class StoreUserRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|email',
            'age' => 'nullable|integer|min:0',
        ];
    }
}

TypeScriptの interface はこれと似た考え方です。「このオブジェクトには name(文字列)と email(文字列)と age(数値、省略可)がある」という形の宣言をコード上で行います。違いは、バリデーションルールが実行時にチェックされるのに対し、TypeScriptは**コンパイル時(コードを書いている最中)**にチェックされる点です。

interface でオブジェクトの形を定義する

interface キーワードを使って、オブジェクトがどんなプロパティを持つかを宣言します。

// ユーザーの「形」を定義
interface User {
  name: string;
  email: string;
  age: number;
}

// この形に合うオブジェクトを作成
const user: User = {
  name: "山田太郎",
  email: "yamada@example.com",
  age: 30,
};

定義した interface に合わないオブジェクトを代入しようとすると、エディタが即座にエラーを表示します。

// エラー:'email' プロパティがありません
const badUser: User = {
  name: "田中花子",
  age: 25,
};

// エラー:'age' は number なのに string を代入している
const badUser2: User = {
  name: "鈴木一郎",
  email: "suzuki@example.com",
  age: "三十", // 型 'string' を型 'number' に割り当てることはできません
};
💡 ヒント

interface 名は慣習的にパスカルケース(先頭大文字)で命名します。IUser のように I プレフィックスを付ける流儀もありますが、現在のTypeScriptコミュニティではプレフィックスなしが主流です。

関数の引数に interface を使う

interface の真価は、関数の引数に使うときに発揮されます。

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

// 引数の型として interface を指定
function greetUser(user: User): string {
  return `こんにちは、${user.name}さん(${user.age}歳)`;
}

// 正しい呼び出し
greetUser({ name: "山田太郎", email: "yamada@example.com", age: 30 });

// エラー:プロパティが足りない
greetUser({ name: "田中花子" });

これにより、関数に渡すオブジェクトの構造がコンパイル時に保証されます。LaravelのFormRequestが実行時にバリデーションエラーを返すのと違い、コードを書いている最中にエディタが教えてくれるのがTypeScriptの強みです。

type エイリアスでオブジェクトの形を定義する

type キーワードでも同じようにオブジェクトの形を定義できます。

// type エイリアスでの定義
type Product = {
  id: number;
  name: string;
  price: number;
  category: string;
};

const product: Product = {
  id: 1,
  name: "TypeScript入門書",
  price: 2980,
  category: "書籍",
};

見た目はほぼ同じですね。interfacetype の違いは後ほど詳しく説明します。

📝 メモ

type はオブジェクトだけでなく、あらゆる型に別名を付けられます。type ID = numbertype Status = "active" | "inactive" のような使い方もできます(ユニオン型は次のチャプターで詳しく扱います)。

optional プロパティ(省略可能なプロパティ)

すべてのプロパティが必須とは限りません。? を付けると「あってもなくてもいい」プロパティになります。

interface UserProfile {
  name: string;       // 必須
  email: string;      // 必須
  age?: number;       // 省略可能(number | undefined)
  bio?: string;       // 省略可能(string | undefined)
  avatarUrl?: string; // 省略可能(string | undefined)
}

// age, bio, avatarUrl は省略できる
const profile1: UserProfile = {
  name: "山田太郎",
  email: "yamada@example.com",
};

// もちろん指定してもOK
const profile2: UserProfile = {
  name: "田中花子",
  email: "tanaka@example.com",
  age: 28,
  bio: "フロントエンドエンジニアです",
};

Laravelのバリデーションルールでいうところの 'nullable' に近い概念です。

⚠️ 注意

optional プロパティにアクセスするときは undefined の可能性を考慮する必要があります。

function displayAge(user: UserProfile): string {
  // user.age は number | undefined なので直接計算できない
  if (user.age !== undefined) {
    return `${user.age}`;
  }
  return "年齢非公開";
}

この「undefinedかもしれない値を安全に扱う」パターンは、TypeScriptで頻繁に出てきます。

readonly プロパティ

readonly を付けると、そのプロパティは作成後に変更できなくなります。

interface Config {
  readonly apiUrl: string;
  readonly apiKey: string;
  timeout: number; // これは変更可能
}

const config: Config = {
  apiUrl: "https://api.example.com",
  apiKey: "secret-key-123",
  timeout: 5000,
};

// OK:readonly でないプロパティは変更できる
config.timeout = 10000;

// エラー:readonly プロパティは変更できない
config.apiUrl = "https://other-api.com";
// 読み取り専用プロパティであるため、'apiUrl' に代入することはできません

Reactのコンポーネントで受け取る props は概念的に readonly です。TypeScriptで React の型定義を見ると、props は Readonly<P> でラップされています。

💡 ヒント

設定値やIDなど「一度決まったら変わらない」値には readonly を付けておくと、意図しない変更をコンパイル時に防げます。Laravelでいえば、$fillable に含めないフィールドのようなものです。

interface の extends(継承)

extends キーワードで既存の interface を拡張して新しい interface を作れます。

// 基本のユーザー型
interface User {
  id: number;
  name: string;
  email: string;
}

// User を拡張して管理者型を定義
interface Admin extends User {
  role: "admin" | "superadmin";
  permissions: string[];
}

// Admin は User のすべてのプロパティ + 追加プロパティを持つ
const admin: Admin = {
  id: 1,
  name: "管理者太郎",
  email: "admin@example.com",
  role: "superadmin",
  permissions: ["users.manage", "posts.delete"],
};

これはPHPやRubyのクラス継承に似た概念です。AdminUser のすべてのプロパティを引き継ぎつつ、独自のプロパティを追加しています。

複数の interface を同時に継承することもできます。

interface Timestamps {
  createdAt: Date;
  updatedAt: Date;
}

interface SoftDeletable {
  deletedAt: Date | null;
}

// User と Timestamps と SoftDeletable を全て継承
interface UserRecord extends User, Timestamps, SoftDeletable {
  isActive: boolean;
}

// UserRecord は全てのプロパティを持つ
const record: UserRecord = {
  id: 1,
  name: "山田太郎",
  email: "yamada@example.com",
  createdAt: new Date(),
  updatedAt: new Date(),
  deletedAt: null,
  isActive: true,
};
📝 メモ

Laravelの Eloquent Model に SoftDeletes トレイトや HasTimestamps を組み合わせるのと似た発想です。TypeScriptではインターフェースの多重継承でこれを表現します。

intersection 型(交差型)

type エイリアスでは & 演算子を使って複数の型を合成できます。これを intersection 型(交差型) と呼びます。

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

type AdminPrivileges = {
  role: "admin" | "superadmin";
  permissions: string[];
};

// & で2つの型を合成する
type Admin = User & AdminPrivileges;

const admin: Admin = {
  id: 1,
  name: "管理者太郎",
  email: "admin@example.com",
  role: "superadmin",
  permissions: ["users.manage", "posts.delete"],
};

extends& は結果的にほぼ同じことを実現しますが、&type エイリアスのみで使える構文です。

// interface の extends と type の & は結果が同じ
interface AdminV1 extends User {
  role: string;
}

type AdminV2 = User & {
  role: string;
};

// どちらも { id, name, email, role } を持つ型になる

ネストしたオブジェクトの型定義

実際のアプリケーションでは、オブジェクトの中にオブジェクトがネストすることがよくあります。

// 住所の型
interface Address {
  postalCode: string;
  prefecture: string;
  city: string;
  street: string;
  building?: string; // 建物名は省略可能
}

// ユーザーの型(住所をネスト)
interface User {
  id: number;
  name: string;
  email: string;
  address: Address; // Address 型をプロパティとして使う
}

const user: User = {
  id: 1,
  name: "山田太郎",
  email: "yamada@example.com",
  address: {
    postalCode: "100-0001",
    prefecture: "東京都",
    city: "千代田区",
    street: "千代田1-1",
    // building は省略可能なので書かなくてOK
  },
};

型を分割して定義することで、コードの見通しが良くなり、Address 型を他の場所でも再利用できます。

配列を含む型定義

配列をプロパティに含める場合の型定義も確認しておきましょう。

interface BlogPost {
  id: number;
  title: string;
  content: string;
  tags: string[];           // 文字列の配列
  comments: Comment[];      // Comment 型の配列
  author: User;             // ネストした型
}

interface Comment {
  id: number;
  text: string;
  author: User;
  createdAt: Date;
}

LaravelでいうEloquentのリレーション定義を思い出してください。hasMany で定義するような1対多の関係は、TypeScriptでは配列型で表現します。

interface と type の使い分け

ここまで interfacetype の両方でオブジェクトの形を定義できることを見てきました。実際の開発では、どちらを使うべきでしょうか?

interface にしかできないこと

宣言のマージ(Declaration Merging)── 同じ名前の interface を複数回宣言すると、自動的にマージされます。

interface User {
  name: string;
}

interface User {
  email: string;
}

// 自動的にマージされ { name: string; email: string } になる
const user: User = {
  name: "山田太郎",
  email: "yamada@example.com",
};

これはライブラリの型定義を拡張するときに便利ですが、アプリケーションコードで意図せず使うとバグの原因になります。

type にしかできないこと

ユニオン型やプリミティブ型のエイリアス── type はあらゆる型に別名を付けられます。

// ユニオン型(次のチャプターで詳しく扱います)
type Status = "active" | "inactive" | "pending";

// プリミティブ型のエイリアス
type ID = number;

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

// これらは interface では定義できない

実践的な使い分けガイドライン

場面推奨理由
オブジェクトの形を定義interface拡張性が高く、エラーメッセージが分かりやすい
React の Props 定義interface or typeプロジェクトで統一されていればどちらでもOK
ユニオン型を定義typeinterface では定義できない
関数の型を定義typetype Handler = (e: Event) => void が自然
既存の型を合成状況による継承関係なら extends、単純な合成なら &
💡 ヒント

チームで統一されていることが最も重要です。迷ったらオブジェクトの形は interface、それ以外は type というルールで始めるのが無難です。React のプロジェクトでは Props の定義に type を使うチームも多いです。

React コンポーネントでの実用例

ここまでの知識を使って、React コンポーネントの Props を型定義してみましょう。

// Props の型を interface で定義
interface ProfileCardProps {
  user: User;
  showEmail?: boolean;  // 省略可能
  onEdit?: () => void;  // コールバック関数(省略可能)
}

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

// コンポーネントに型を適用
function ProfileCard({ user, showEmail = false, onEdit }: ProfileCardProps) {
  return (
    <div className="profile-card">
      {user.avatarUrl && <img src={user.avatarUrl} alt={user.name} />}
      <h2>{user.name}</h2>
      {showEmail && <p>{user.email}</p>}
      {onEdit && <button onClick={onEdit}>編集</button>}
    </div>
  );
}

interface で Props を定義することで、コンポーネントを使う側でもエディタの補完が効き、渡し忘れや型のミスを防げます。

✍ やってみよう:ユーザーとアドレスの型を定義する

以下の要件を満たす型定義を作成してください。

1. Address 型を定義する

  • postalCode: 文字列(必須)
  • prefecture: 文字列(必須)
  • city: 文字列(必須)
  • street: 文字列(必須)
  • building: 文字列(省略可能)

2. User interface を定義する

  • id: 数値(readonly)
  • name: 文字列(必須)
  • email: 文字列(必須)
  • age: 数値(省略可能)
  • address: Address 型(省略可能)
  • hobbies: 文字列の配列(省略可能)

3. Admin interface を定義する

  • User を継承する
  • role: "admin" または "superadmin"(リテラル型)
  • permissions: 文字列の配列(必須)
  • lastLoginAt: Date 型(省略可能)

4. 以下の関数を実装する

// ユーザーの表示名を返す関数
function getDisplayName(user: User): string {
  // 名前と年齢(あれば)を組み合わせて返す
  // 例: "山田太郎(30歳)" or "山田太郎"
}

// 管理者かどうかを判定する関数(型の互換性を活かす)
function isHighPrivilege(admin: Admin): boolean {
  // role が "superadmin" なら true を返す
}

ヒント:AdminUser を継承しているため、Admin 型の値は User を受け取る関数にも渡せます。これはPHPやRubyのポリモーフィズムと同じ考え方です。

まとめ

このチャプターで学んだことをまとめます:

次のチャプターでは、ユニオン型とリテラル型 を学びます。"success" | "error" | "loading" のように、変数が取りうる値を制限する強力な仕組みです。