はじめに#
TypeScriptでオブジェクトの型を定義する方法として、type(型エイリアス)とinterfaceの2つがあります。どちらもオブジェクトの構造を定義できますが、それぞれに固有の特徴があり、状況によって使い分けが必要です。
本記事では、TypeScriptのtypeとinterfaceの違いを詳しく解説し、ユニオン型・交差型での使い分け、宣言マージの挙動、パフォーマンスの観点、そしてプロジェクトでの命名規則まで網羅的に説明します。
この記事を読み終えると、以下のことができるようになります。
typeとinterfaceそれぞれの基本構文と特徴を理解できる
- 拡張方法の違い(
extends vs 交差型)を使い分けられる
- 宣言マージが必要な場面で
interfaceを適切に選択できる
- ユニオン型やプリミティブ型のエイリアスには
typeを使うべき理由がわかる
- プロジェクトのコーディング規約に沿った命名規則を適用できる
実行環境・前提条件#
前提知識#
動作確認環境#
| ツール |
バージョン |
| Node.js |
20.x以上 |
| TypeScript |
5.7以上 |
| VS Code |
最新版 |
本記事のサンプルコードは、TypeScript Playgroundで動作確認できます。ローカル環境で実行する場合は、開発環境構築ガイドを参照してください。
期待される結果#
本記事のコードを実行すると、以下の動作を確認できます。
typeとinterfaceの両方でオブジェクト型を定義し、同等に使用できる
- 拡張方法の違いによるコンパイル結果の差異を確認できる
- 宣言マージの挙動を理解し、意図した型定義ができる
TypeScriptにおけるtypeとinterfaceの基本#
まず、typeとinterfaceの基本的な構文を確認しましょう。
graph TB
A[TypeScript オブジェクト型定義] --> B[interface]
A --> C[type]
B --> D["interface User {<br/> name: string;<br/> age: number;<br/>}"]
C --> E["type User = {<br/> name: string;<br/> age: number;<br/>}"]interfaceの基本構文#
interfaceはオブジェクトの形状(Shape)を定義するための専用構文です。
1
2
3
4
5
6
7
8
9
10
|
interface User {
name: string;
age: number;
email?: string; // オプショナルプロパティ
}
const user: User = {
name: "田中太郎",
age: 30
};
|
typeの基本構文#
type(型エイリアス)は、任意の型に名前を付けるための構文です。
1
2
3
4
5
6
7
8
9
10
|
type User = {
name: string;
age: number;
email?: string;
};
const user: User = {
name: "田中太郎",
age: 30
};
|
オブジェクト型の定義においては、typeとinterfaceは見た目も機能もほぼ同等です。しかし、それぞれに固有の特徴があります。
typeとinterfaceの主要な違い#
TypeScriptのtypeとinterfaceには、いくつかの重要な違いがあります。以下の表で概要を把握しましょう。
| 機能 |
interface |
type |
| オブジェクト型の定義 |
可能 |
可能 |
| 拡張方法 |
extendsキーワード |
交差型(&) |
| 宣言マージ |
可能 |
不可 |
| ユニオン型の定義 |
不可 |
可能 |
| プリミティブ型のエイリアス |
不可 |
可能 |
| タプル型の定義 |
不可 |
可能 |
implementsでの使用 |
可能 |
可能(オブジェクト型の場合) |
| コンパイラパフォーマンス |
優れている |
やや劣る(複雑な交差型の場合) |
それぞれの違いについて、詳しく見ていきましょう。
型の拡張方法の違い#
interfaceとtypeでは、既存の型を拡張する方法が異なります。
interfaceのextends#
interfaceはextendsキーワードを使って他のinterfaceを継承できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
bark(): void;
}
const dog: Dog = {
name: "ポチ",
breed: "柴犬",
bark() {
console.log("ワンワン!");
}
};
|
複数のinterfaceを同時に継承することも可能です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
interface Flyable {
fly(): void;
}
interface Swimmable {
swim(): void;
}
interface Duck extends Animal, Flyable, Swimmable {
quack(): void;
}
const duck: Duck = {
name: "ドナルド",
fly() {
console.log("飛んでいます");
},
swim() {
console.log("泳いでいます");
},
quack() {
console.log("ガーガー!");
}
};
|
typeの交差型(Intersection Types)#
typeは交差型(&)を使って複数の型を組み合わせます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
type Animal = {
name: string;
};
type Dog = Animal & {
breed: string;
bark(): void;
};
const dog: Dog = {
name: "ポチ",
breed: "柴犬",
bark() {
console.log("ワンワン!");
}
};
|
同様に、複数の型を組み合わせることができます。
1
2
3
4
5
6
7
8
9
10
11
|
type Flyable = {
fly(): void;
};
type Swimmable = {
swim(): void;
};
type Duck = Animal & Flyable & Swimmable & {
quack(): void;
};
|
extendsと交差型の違い#
一見同じように見えるextendsと交差型ですが、プロパティの競合時の挙動が異なります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// interfaceの場合:競合するとコンパイルエラー
interface Base {
id: number;
}
// エラー: Interface 'Extended' incorrectly extends interface 'Base'.
// interface Extended extends Base {
// id: string; // 型が競合
// }
// typeの場合:交差型は両方の条件を満たす型になる
type BaseType = {
id: number;
};
type ExtendedType = BaseType & {
id: string;
};
// ExtendedType の id は number & string = never となる
|
interfaceのextendsは、競合を早期にエラーとして検出できるため、意図しない型の競合を防げます。
宣言マージ(Declaration Merging)#
interfaceの最大の特徴の一つが「宣言マージ」です。同じ名前のinterfaceを複数回宣言すると、自動的にマージされます。
interfaceの宣言マージ#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
interface User {
name: string;
}
interface User {
age: number;
}
interface User {
email?: string;
}
// 上記3つの宣言がマージされ、以下と同等になる
// interface User {
// name: string;
// age: number;
// email?: string;
// }
const user: User = {
name: "田中太郎",
age: 30
// emailは省略可能
};
|
typeでは宣言マージができない#
typeは同じ名前で再宣言するとコンパイルエラーになります。
1
2
3
4
5
6
7
8
|
type User = {
name: string;
};
// エラー: Duplicate identifier 'User'.
// type User = {
// age: number;
// };
|
宣言マージの実用的なユースケース#
宣言マージは、以下のような場面で活用されます。
1. ライブラリの型拡張#
外部ライブラリの型を拡張する際に宣言マージが役立ちます。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// Express.jsのRequest型を拡張する例
declare global {
namespace Express {
interface Request {
user?: {
id: string;
name: string;
};
}
}
}
// これにより、req.user にアクセスできるようになる
|
2. グローバル型の拡張#
Window オブジェクトに独自のプロパティを追加する例です。
1
2
3
4
5
6
7
8
9
|
interface Window {
myApp: {
version: string;
init(): void;
};
}
// TypeScriptがwindow.myAppを認識するようになる
window.myApp.init();
|
3. モジュール型の拡張#
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 元のモジュール定義
declare module "some-library" {
interface Config {
apiKey: string;
}
}
// 別ファイルで拡張
declare module "some-library" {
interface Config {
debug?: boolean;
}
}
|
ユニオン型とtypeの関係#
typeの最大の強みは、ユニオン型を定義できることです。interfaceではこれができません。
ユニオン型の定義#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// typeでユニオン型を定義
type Status = "pending" | "approved" | "rejected";
type Result =
| { success: true; data: string }
| { success: false; error: Error };
// 使用例
function handleResult(result: Result) {
if (result.success) {
console.log(result.data);
} else {
console.error(result.error.message);
}
}
|
Discriminated Unions(判別可能なユニオン型)#
TypeScriptで頻繁に使用されるパターンです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
}
}
|
interfaceでユニオン型を表現する場合の制限#
interface単体ではユニオン型を定義できませんが、typeと組み合わせることは可能です。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
interface Circle {
kind: "circle";
radius: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
// interfaceをtypeでユニオンにまとめる
type Shape = Circle | Rectangle;
|
プリミティブ型・タプル型のエイリアス#
typeはプリミティブ型やタプル型にもエイリアスを付けられます。interfaceにはできないことです。
プリミティブ型のエイリアス#
1
2
3
4
5
6
7
8
9
10
|
// プリミティブ型にセマンティックな名前を付ける
type UserId = string;
type Age = number;
type IsActive = boolean;
// 関数シグネチャの可読性が向上
function getUser(id: UserId): { name: string; age: Age } {
// 実装
return { name: "田中太郎", age: 30 };
}
|
リテラル型のユニオン#
1
2
3
4
5
6
7
|
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
type StatusCode = 200 | 201 | 400 | 401 | 403 | 404 | 500;
function makeRequest(method: HttpMethod, url: string): Promise<StatusCode> {
// 実装
return Promise.resolve(200);
}
|
タプル型の定義#
1
2
3
4
5
6
7
|
type Coordinate = [number, number];
type RGB = [number, number, number];
type NameAgePair = [string, number];
const point: Coordinate = [10, 20];
const color: RGB = [255, 128, 0];
const person: NameAgePair = ["田中太郎", 30];
|
複雑なタプル型#
1
2
3
4
5
6
7
8
|
// 可変長タプル
type StringNumberBooleans = [string, number, ...boolean[]];
// ラベル付きタプル(TypeScript 4.0以降)
type Point3D = [x: number, y: number, z: number];
// readonly タプル
type ReadonlyCoordinate = readonly [number, number];
|
コンパイラパフォーマンスの違い#
TypeScriptの公式ドキュメントでは、interfaceのextendsを使用した方が、typeの交差型よりもコンパイラのパフォーマンスが優れていると述べられています。
graph LR
A[型定義] --> B{複雑な型?}
B -->|Yes| C[interface + extends<br/>推奨]
B -->|No| D[どちらでも可]
C --> E[キャッシュされやすい]
D --> F[type or interface]なぜinterfaceの方がパフォーマンスが良いのか#
interfaceは名前付きの型として扱われ、TypeScriptコンパイラ内部でキャッシュされやすい構造になっています。一方、交差型は評価のたびに計算が必要になる場合があります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// パフォーマンスの観点から推奨
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// 複雑なプロジェクトでは避けた方が良い場合もある
type AnimalType = {
name: string;
};
type DogType = AnimalType & {
breed: string;
};
|
ただし、通常の規模のプロジェクトでは、この差は体感できないほど小さいです。パフォーマンスを理由に選択を変える必要があるのは、非常に大規模なプロジェクトの場合のみです。
クラスでの使用(implements)#
interfaceとtypeの両方をクラスでimplementsできます。
interfaceをimplementsする#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
interface Printable {
print(): void;
}
interface Serializable {
serialize(): string;
}
class Document implements Printable, Serializable {
constructor(private content: string) {}
print(): void {
console.log(this.content);
}
serialize(): string {
return JSON.stringify({ content: this.content });
}
}
|
typeをimplementsする#
オブジェクト型のtypeであれば、同様にimplementsできます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
type Printable = {
print(): void;
};
type Serializable = {
serialize(): string;
};
class Document implements Printable, Serializable {
constructor(private content: string) {}
print(): void {
console.log(this.content);
}
serialize(): string {
return JSON.stringify({ content: this.content });
}
}
|
ただし、ユニオン型やプリミティブ型のエイリアスはimplementsできません。
1
2
3
4
|
type Status = "active" | "inactive";
// エラー: A class can only implement an object type or intersection of object types with statically known members.
// class StatusClass implements Status {}
|
プロジェクトでの命名規則#
TypeScriptプロジェクトでは、typeとinterfaceの命名規則を統一することが重要です。
Microsoftの推奨ガイドライン#
Microsoftの公式ガイドラインでは、以下の命名規則が推奨されています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// 推奨:プレフィックスなし
interface User {
name: string;
}
type UserStatus = "active" | "inactive";
// 非推奨:Iプレフィックス(C#スタイル)
// interface IUser {
// name: string;
// }
// 非推奨:Typeサフィックス
// type UserType = { name: string };
|
実践的な命名規則#
プロジェクトで統一すべき命名規則の例を紹介します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// エンティティ・モデル
interface User { ... }
interface Product { ... }
interface Order { ... }
// Props(React)
interface ButtonProps { ... }
interface ModalProps { ... }
// ユニオン型(状態、ステータス)
type UserRole = "admin" | "user" | "guest";
type LoadingState = "idle" | "loading" | "success" | "error";
// 関数型
type EventHandler = (event: Event) => void;
type AsyncFunction<T> = () => Promise<T>;
// ユーティリティ型(プロジェクト固有)
type Nullable<T> = T | null;
type Optional<T> = T | undefined;
|
使い分けの判断フローチャート#
以下のフローチャートを参考に、typeとinterfaceを使い分けましょう。
flowchart TD
A[型を定義したい] --> B{どのような型?}
B -->|オブジェクト型| C{宣言マージが必要?}
B -->|ユニオン型| D[type を使用]
B -->|タプル型| D
B -->|プリミティブのエイリアス| D
B -->|関数型| D
C -->|Yes| E[interface を使用]
C -->|No| F{拡張が必要?}
F -->|Yes| G{extends を使いたい?}
F -->|No| H[どちらでも可<br/>プロジェクト規約に従う]
G -->|Yes| E
G -->|No| I[type + 交差型]使い分けのまとめ#
interfaceを使うべき場面#
- オブジェクトの形状を定義する(基本的なケース)
- 宣言マージが必要な場合(ライブラリの型拡張など)
extendsによる明確な継承関係を表現したい場合
- 大規模プロジェクトでコンパイラパフォーマンスを重視する場合
typeを使うべき場面#
- ユニオン型を定義する場合
- タプル型を定義する場合
- プリミティブ型にセマンティックな名前を付ける場合
- 関数型を定義する場合
- 複雑な型の組み合わせ(交差型、条件付き型など)
実践的なコード例#
実際のプロジェクトでの使い分け例を見てみましょう。
API レスポンスの型定義#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
// 基本的なエンティティはinterface
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
interface Post {
id: string;
title: string;
content: string;
authorId: string;
publishedAt: Date | null;
}
// APIレスポンスのラッパーはtype(ジェネリクスとの組み合わせ)
type ApiResponse<T> =
| { status: "success"; data: T }
| { status: "error"; error: { code: string; message: string } };
// 使用例
type UserResponse = ApiResponse<User>;
type PostListResponse = ApiResponse<Post[]>;
async function fetchUser(id: string): Promise<UserResponse> {
try {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return { status: "success", data };
} catch (e) {
return {
status: "error",
error: { code: "FETCH_ERROR", message: "Failed to fetch user" }
};
}
}
|
Reactコンポーネントの型定義#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// Propsはinterfaceで定義
interface ButtonProps {
label: string;
onClick: () => void;
variant?: "primary" | "secondary" | "danger";
disabled?: boolean;
children?: React.ReactNode;
}
// イベントハンドラーの型はtype
type ClickHandler = (event: React.MouseEvent<HTMLButtonElement>) => void;
// コンポーネントの状態のユニオン型はtype
type ButtonState = "idle" | "loading" | "success" | "error";
// 複合的な型はtypeで組み合わせ
type ButtonPropsWithState = ButtonProps & {
state: ButtonState;
};
|
状態管理の型定義#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
// Reduxスタイルの状態管理
interface UserState {
currentUser: User | null;
isLoading: boolean;
error: string | null;
}
// アクション型はtypeでユニオン
type UserAction =
| { type: "LOGIN_START" }
| { type: "LOGIN_SUCCESS"; payload: User }
| { type: "LOGIN_FAILURE"; payload: string }
| { type: "LOGOUT" };
// Reducerの型定義
type UserReducer = (state: UserState, action: UserAction) => UserState;
const userReducer: UserReducer = (state, action) => {
switch (action.type) {
case "LOGIN_START":
return { ...state, isLoading: true, error: null };
case "LOGIN_SUCCESS":
return { ...state, isLoading: false, currentUser: action.payload };
case "LOGIN_FAILURE":
return { ...state, isLoading: false, error: action.payload };
case "LOGOUT":
return { ...state, currentUser: null };
default:
return state;
}
};
|
まとめ#
TypeScriptのtypeとinterfaceは、多くの場面で互換的に使用できますが、それぞれに固有の特徴があります。
本記事で解説した主要なポイントを振り返りましょう。
- オブジェクト型の定義:
typeとinterfaceの両方で可能
- 拡張方法:
interfaceはextends、typeは交差型(&)
- 宣言マージ:
interfaceのみ可能(ライブラリ拡張に有用)
- ユニオン型・タプル型:
typeのみで定義可能
- コンパイラパフォーマンス:大規模プロジェクトでは
interfaceが有利
- 命名規則:プレフィックス(
Iなど)は使わないのが現代の推奨
プロジェクトでは、チーム内で一貫した規約を設け、どちらを使うかを明確にすることが重要です。TypeScript公式ドキュメントでは「迷ったらinterfaceを使い、typeの機能が必要になったらtypeを使う」というヒューリスティックが提案されています。
次回は、ユニオン型とリテラル型について、より詳しく解説していきます。
参考リンク#