1. はじめに
TypeScriptと「型」について
TypeScriptは、JavaScriptに静的型システムを追加したプログラミング言語です。
静的型システム = プログラムを実行する前(コンパイル時など)に、値や変数の型が正しいかをチェックする仕組み
ここでいう「型」とは、変数や関数の引数・戻り値に「どのようなデータ(数値、文字列、オブジェクトなど)が入るか」をあらかじめ定義する仕組みのことです。
型を定義しておくことで、コードの実行前(コンパイル時)にバグや型違いのエラーを検知でき、開発の安全性と効率が大幅に向上します。
この記事を書こうと思った背景
実務でTypeScriptを使用していると、同じようなオブジェクトの型定義にinterfaceとtypeの両方が使われていることがあります。
例えば、以下の2つはどちらもUserというオブジェクトの型を定義しています。
interface User {
id: number;
name: string;
}
type User = {
id: number;
name: string;
};
このように、基本的なオブジェクトの型定義だけを見ると、interfaceとtypeの違いは分かりにくいです。
そのため、この記事で自分の理解を深めるために、両者の違いについてまとめていきます。
本記事では、単なる文法的な違いだけでなく、誕生の歴史的背景、エラーメッセージやコンパイルパフォーマンスの違い、クラス(class)との関係までを踏まえ、実務で迷わないための明確な使い分け基準を解説します。
2. Interfaceとtypeの対比テーブル&公式指針
最初に、interface と type の違いを対比テーブルでまとめます。
機能と特徴の比較一覧
| 機能・特徴 | Interface | type(型エイリアス) |
|---|---|---|
| オブジェクトの型定義 | ○ | ○ |
| 関数の型定義 | ○ | ○ |
| プリミティブ型の別名定義 | × | ○ |
| Union型(共用体型)の定義 | × | ○ |
| 型継承・拡張の仕組み | extends キーワード | Intersection型(&) |
| 同名定義時のマージ | ○(Declaration Merging) | ×(重複エラー) |
| エラー表示・キャッシュ構造 | 名前が保持されやすくキャッシュされる | 展開・再帰計算される場合がある |
| 型の性質(Open / Closed) | Open(後から拡張可能) | Closed(一度定義したら不変) |
公式ハンドブックが示す判断の目安
TypeScript公式ハンドブックでは、以下のような指針が示されています。
「基本的には好みに応じて選択して問題ありません。どちらか別の宣言が必要な場合はTypeScriptが教えてくれます。もし判断に迷う場合の目安としては、
type 特有の機能が必要になるまでは interface を使用することをおすすめします。」
3. Interfaceとtypeが生まれた歴史的背景
なぜTypeScriptには似たような2つの型定義手段が存在するのでしょうか?
-
2012年:TypeScript公開と interface の登場
TypeScriptが2012年に初めて公開された時から存在していたのが interface です。主にオブジェクト指向言語の概念をベースにしており、「オブジェクトの形状」や「クラスが満たすべき契約」を定義するために設計されました。 -
2014年:TypeScript 1.4での type(型エイリアス)の追加
初期は interface が主流でしたが、TypeScript 1.4で type(型エイリアス)が追加されました。これにより、単なるプリミティブ型への命名だけでなく、
Union型(|)などの導入によって型の表現力が飛躍的に広がりました。
4. Interfaceでできることと特徴
interface は、主にオブジェクトの構造やクラスの設計図を定義するのに適しています。
オブジェクトの型定義
プロパティの型、オプショナル(?)、読み取り専用(readonly)などを定義できます。
interface User {
id: number;
name: string;
email?: string; // オプショナルプロパティ
readonly createdAt: Date; // 変更不可
}
関数の型定義
interface では**コールシグネチャ(Call Signature)**という構文を使って関数の型を定義できます。
コールシグネチャとは?
オブジェクトの型定義の中で(引数): 戻り値の型のように記述する特殊な構文です。「このオブジェクト自体が関数として呼び出し可能である」ことを表現します。
// コールシグネチャによる関数の型定義
interface Add {
(a: number, b: number): number;
}
const addFn: Add = (a, b) => a + b;
extends による型の継承・拡張
extends キーワードを使って、既存の interface を継承できます。
interface User {
id: number;
name: string;
}
// Userを継承してAdminを作成
interface Admin extends User {
role: string;
}
Declaration Merging(宣言のマージ)
同じ名前の interface を複数定義すると、自動的にプロパティが統合(マージ)されます。
interface User {
id: number;
}
interface User {
name: string;
}
// Userは { id: number; name: string; } として扱われる
Declaration Mergingを利用すると、外部ライブラリが提供しているinterfaceに、自分で必要なプロパティを後から追加できます。
5. type(型エイリアス)でできることと特徴
type は、あらゆる型に名前をつけたり、柔軟に組み合わせたりするのに適しています。
プリミティブ型の別名定義
数値や文字列などの基本型(プリミティブ型)に、意味のある名前をつけることができます。※ interface では不可能です。
type UserId = number;
type UserName = string;
Union型の定義
「AまたはB」という複数の型・値を許可する定義が可能です。
type Status = 200 | 400 | 500;
type Result = "success" | "error";
オブジェクトおよび関数の型定義
interface と同様に、オブジェクトや関数の型も定義できます。
// オブジェクト型
type User = {
id: number;
name: string;
};
// 関数型(アロー関数風の記法)
type Add = (a: number, b: number) => number;
Intersection型による型の組み合わせ
extends の代わりに、Intersection型(&)を使って複数の型を合成できます。
type BaseUser = { id: number; name: string; };
type Role = { role: string; };
type Admin = BaseUser & Role;
6. コンパイルパフォーマンスと表示の違い
interface の extends と type の Intersection型(&)は、どちらも型を組み合わせる機能です。
以前のTypeScriptでは、複雑な Intersection 型はコンパイラの型チェックに負荷がかかりやすかったため、公式でも interface の extends が推奨されていました。
現在はコンパイラの進化により速度差を過剰に意識する必要は薄れていますが、パフォーマンス以外の「エラー表示や挙動の違い」が存在します。
-
型衝突時の安全性の違い
-
interface extends: 同名プロパティで型の衝突が起きると明示的なエラーを発生させます。 -
Intersection (&): 衝突してもエラーにならず、プロパティがnever型になって意図しないバグを生むことがあります。
-
-
エラーメッセージ・ホバー表示の違い
-
interface: エラー時やエディタのホバー表示で元の型名が保持されやすく、視認性が高く保たれます。 -
Intersection (&): 複雑に合成すると内部構造がそのまま展開され、エラーログが読みづらくなる場合があります。
-
// --- 1. interface extends の場合 ---
interface ParentInterface {
id: number;
}
// 宣言した瞬間に定義部分に赤波線エラーが発生します
interface ChildInterface extends ParentInterface {
id: string;
// エラー: インターフェース 'ChildInterface' はインターフェース 'ParentInterface' を正しく拡張していません。
}
// --- 2. type (Intersection型) の場合 ---
type ParentType = {
id: number;
};
// 型の定義時点ではエラーになりません(スルーされます)
type ChildType = ParentType & {
id: string;
};
// 値を代入しようとして初めて、id が never 型になっているエラーが発生します
const child: ChildType = {
id: "abc", // エラー: 型 'string' を型 'never' に割り当てることはできません。
};
【選定の指針】
パフォーマンスのためだけに interface を強制する必要はありませんが、型衝突の検知やエラーメッセージの読みやすさを重視する場合は interface extends、柔軟な型の組み合わせを重視する場合は type (&) を選択するのが適しています。
7. 補足:class(クラス)も型として使える
型定義といえば interface と type が注目の中心になりますが、TypeScriptでは class も型として利用可能です。
class を宣言すると、JavaScriptとして実行される「インスタンス生成コード(値)」と、その「インスタンスの型」が同時に定義されます。
class Product {
constructor(public id: number, public name: string) {}
}
// Productクラス自体を「型」として利用できる
const item: Product = new Product(1, "KeyBoard");
8. 実務での使い分けガイドライン&まとめ
interface と type には機能の重複も多いため、プロジェクトやチーム内でルールを統一することが重要です。
代表的なチームルール例
パターンA:公式推奨ベースの標準スタイル(推奨)
- オブジェクトの構造(APIレスポンス、コンポーネントProps、データモデル等) → interface
- Union型、プリミティブの別名、Mapped Types等 → type
- 判断に迷った時 → まず interface を検討し、type でしか表現できない機能が必要になったら type に切り替える。
パターンB:全般 type 優先スタイル
- 記述を一貫させるため原則 type を使用し、Declaration Mergingが必要なフレームワーク/ライブラリ開発のみ interface を使用する。
まとめ
-
interface の特徴:
- 2012年のTS公開時から存在
- オブジェクトの構造定義が得意
- extends による拡張はコンパイラのキャッシュが効きやすく、衝突エラーも検知しやすい
- 同名宣言による Declaration Merging が可能
-
type の特徴:
- TS 1.4(2014年)で追加され、型の表現力を大幅に広げた
- プリミティブ型の別名、Union型(|)、Intersection型(&)など柔軟な定義が可能
- 一度定義したら拡張・マージできない(Closedな)安全な型を作れる
-
補足:
- class も値と型の両方として機能する
最終的な指針:
「オブジェクトの構造定義や継承にはコンパイル面でも強い interface を優先し、Union型や組み合わせなどの柔軟な型表現には type を使う」 という意識を持つと、クリーンで堅牢なTypeScriptコードが書けるようになります!