TypeScript でオブジェクトの「型」を定義する方法には、type(型エイリアス)と interface(インターフェース)の2つがあります。どちらも似たような書き方ができるため、「結局どちらを使えばいいの?」と迷いがちです。この記事では、それぞれの基本的な書き方から、拡張のしかた、両者にしかできないこと、そして実際にどう使い分けるかまでを、具体的なコード例とともに整理します。読み終えるころには、自信を持って選べるようになるはずです。
目次
type と interface の基本の書き方
まずはどちらもオブジェクトの形(プロパティの名前と型)を定義できる、という共通点から見ていきます。同じ「ユーザー」を表す型を、両方で書いてみます。
// type(型エイリアス)で定義
type User = {
id: number;
name: string;
active: boolean;
};
// interface で定義
interface UserInterface {
id: number;
name: string;
active: boolean;
}
// どちらも同じように使える
const taro: User = { id: 1, name: 'Taro', active: true };
const hanako: UserInterface = { id: 2, name: 'Hanako', active: false };
見た目の違いは、type が = で型を「代入」する形なのに対し、interface は = を使わずブロックで宣言する点です。この程度のオブジェクト型であれば、どちらを選んでも動作はまったく同じで、使う側のコードも変わりません。違いが出てくるのは、型を組み合わせたり拡張したりする場面です。
type だけができること
type は「型に別名を付ける」機能なので、オブジェクトに限らずあらゆる型に名前を付けられます。interface では書けない、次のような型は type の得意分野です。
// ユニオン型(複数の型のどれか)
type Status = 'todo' | 'doing' | 'done';
// プリミティブ型やタプルにも名前を付けられる
type ID = string | number;
type Point = [number, number];
// 既存の型から新しい型を組み立てる
type Nullable<T> = T | null;
type UserName = User['name']; // User の name プロパティの型 = string
'todo' | 'doing' | 'done' のようなユニオン型(複数の候補のうちどれか1つ)や、[number, number] のようなタプル型は interface では表現できません。「文字列か数値のどちらか」といった柔軟な型を扱いたいときは type を使うことになります。
拡張のしかたの違い
既存の型に項目を足して新しい型を作る「拡張」は、どちらでも可能ですが書き方が異なります。interface は extends キーワードを使い、type は &(インターセクション型)で複数の型を合成します。
// interface は extends で拡張する
interface Animal {
name: string;
}
interface Dog extends Animal {
bark: () => void;
}
// type は &(インターセクション)で合成する
type AnimalType = {
name: string;
};
type DogType = AnimalType & {
bark: () => void;
};
結果として得られる Dog と DogType はほぼ同じですが、interface の extends のほうが「継承」の意図が読み取りやすく、エラーメッセージも分かりやすくなる傾向があります。クラスの世界に近い書き方だと感じる人も多いでしょう。
interface だけができる「宣言のマージ」
interface には type にない特徴があります。それが宣言のマージ(Declaration Merging)です。同じ名前の interface を複数回宣言すると、TypeScript がそれらを自動的に1つに合体させます。
interface Window {
title: string;
}
interface Window {
version: number;
}
// 2つの宣言がマージされ、両方のプロパティを持つ
const w: Window = { title: 'App', version: 1 };
一方、type で同じ名前を2回宣言すると「識別子が重複しています」というエラーになります。宣言のマージは、ライブラリが用意している型(たとえばブラウザの Window や外部パッケージの型)に、あとから自分のプロパティを追加したいときに役立ちます。ライブラリの型を拡張する必要がある場面では interface が必須になることを覚えておきましょう。
結局どちらを使えばいい?
できることの違いを表にまとめます。日常のコードでは重ならない部分が多いので、迷ったときの判断材料にしてください。
| やりたいこと | type | interface |
|---|---|---|
| オブジェクトの形を定義 | できる | できる |
| ユニオン型・タプル型 | できる | できない |
| プリミティブ型に別名 | できる | できない |
| 拡張 | & で合成 | extends |
| 同名宣言のマージ | できない(エラー) | できる |
実務では、次のような方針にすると判断がぶれにくくなります。まず、オブジェクトの形を表すなら基本は好みで統一してよいですが、多くのプロジェクトでは「拡張やマージがしやすい」という理由でオブジェクト型に interface を採用しています。そのうえで、ユニオン型・タプル型・関数型・条件型など「オブジェクト以外の型」や「型の組み立て」が必要なときは type を使います。この2つを軸にすれば、ほとんどの場面で悩まずに選べます。
大切なのは、チームやプロジェクトの中でルールを1つに決めて統一することです。どちらにも合理的な使い方があるため、混在してもすぐ困るわけではありませんが、方針を決めておくとコードレビューやメンテナンスがぐっと楽になります。
まとめ
type と interface は、オブジェクトの形を定義するという点では同じように使えます。違いは、type がユニオン型やタプルなど「あらゆる型」に名前を付けられる柔軟さを持ち、interface は extends による拡張と、同名宣言を合体させる「宣言のマージ」ができる点です。目安としては、オブジェクト型は interface(拡張・マージに強い)、オブジェクト以外の型や型の組み立ては type、と分けるとすっきりします。最終的にはプロジェクトで方針を統一することがいちばん重要です。