type-vs-interface について
型の宣言や合成で `type` と `interface` を選ぶ方法を解説するTypeScriptスキルです。オブジェクト型、ユニオン、タプル、拡張可能なAPIを扱う開発者向けに判断基準を示します。
主な機能
- 01型の形と用途に応じた選択
- 02拡張可能なオブジェクトにinterface
- 03ユニオンやタプルに型エイリアス
- 04宣言マージの理解
- 05拡張や交差型による合成
活用シーン
- 01公開APIのオブジェクト型定義
- 02ユニオン型やタプルデータの表現
- 03ライブラリ型の宣言方法の選択
- 04コードベース内の型選択の統一
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill type-vs-interfaceインストール後、Claude Code で/type-vs-interfaceを入力して読み込まれたか確認します。
詳細
typeとinterfaceの違いを知る
概要
オブジェクト型にはinterfaceを、それ以外にはtypeを使います。
typeとinterfaceはどちらもオブジェクト型を定義できますが、それぞれ異なる機能があります。こうした違いを理解しておくと、適切な方を選び、一貫性のあるコードを書けます。
このスキルを使う場面
- 名前付きの型を定義するとき
- 型エイリアスとインターフェースのどちらを使うか選ぶとき
- 型を拡張したり合成したりするとき
- 宣言ファイルを扱うとき
- ライブラリの型定義を理解するとき
絶対に守るルール
Use INTERFACE for object types that could be extended.
Use TYPE for unions, tuples, mapped types, and complex compositions.
覚えておきましょう:
- インターフェースは宣言マージをサポートします
- 型エイリアスはより複雑な型を表現できます
- どちらも拡張および実装できます
- コードベース全体で一貫性を保つことが最も重要です
簡単な選択ガイド
| シナリオ | 使用するもの |
|---|---|
| オブジェクト型(APIレスポンス、props) | interface |
| ユニオン型 | type |
| タプル型 | type |
| 関数型 | type |
| マップ型 | type |
| プリミティブ型のエイリアス | type |
| 拡張を想定したライブラリの型 | interface |
共通点
どちらもオブジェクトの形状を定義できる
type TState = {
name: string;
capital: string;
};
interface IState {
name: string;
capital: string;
}
// These are interchangeable for most purposes
どちらもジェネリクスをサポートする
type TBox<T> = { value: T };
interface IBox<T> { value: T }
どちらも拡張できる
// Interface extending type
interface IStateWithPop extends TState {
population: number;
}
// Type extending interface
type TStateWithPop = IState & { population: number };
どちらも実装できる
class StateImpl implements IState {
name = '';
capital = '';
}
違い
1. ユニオン型(typeのみ)
type StringOrNumber = string | number; // Only with type
type Status = 'pending' | 'fulfilled' | 'rejected';
// Can't do this with interface
2. タプル型(typeのみ)
type Pair = [number, number];
type NamedNums = [string, ...number[]];
// Interface syntax is awkward
interface IPair {
0: number;
1: number;
length: 2;
}
3. 宣言マージ(interfaceのみ)
interface User {
name: string;
}
interface User {
email: string;
}
// Now User has both name and email
const user: User = { name: 'Alice', email: '[email protected]' };
これは、TypeScriptがESのバージョン間で標準ライブラリの型を拡張する仕組みです。
4. よりわかりやすいエラーメッセージ(interface)
interface Person {
name: string;
age: string; // Note: string
}
// Type intersection silently creates unusable type
type TPerson = Person & { age: number }; // No error, but age is never
// Interface extension gives helpful error
interface IPerson extends Person {
age: number;
// ~~~ Types of property 'age' are incompatible
}
5. 型エイリアスのインライン化
TypeScriptは、エラーメッセージや.d.tsファイル内で型エイリアスをインライン化することがあります。
// In generated .d.ts, type aliases may be expanded
type Point = { x: number; y: number };
export function getOrigin(): Point { ... }
// May become:
export function getOrigin(): { x: number; y: number };
// Interfaces are preserved by name
interface IPoint { x: number; y: number }
export function getOrigin(): IPoint { ... }
// Stays as:
export function getOrigin(): IPoint;
interfaceを使う場面
公開APIを定義する
// Users might want to extend this
export interface RequestOptions {
url: string;
method?: string;
headers?: Record<string, string>;
}
// Declaration merging allows extension
declare module 'my-lib' {
interface RequestOptions {
timeout?: number; // User adds this
}
}
一般的なオブジェクト型
interface User {
id: string;
name: string;
email: string;
}
interface Post {
id: string;
title: string;
author: User;
}
typeを使う場面
ユニオン型
type Result<T> = { ok: true; value: T } | { ok: false; error: Error };
type Status = 'loading' | 'success' | 'error';
タプルと配列
type Point = [x: number, y: number];
type RGB = [number, number, number];
関数型
type Handler = (event: Event) => void;
type AsyncFn<T> = () => Promise<T>;
マップ型と条件型
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type NonNullable<T> = T extends null | undefined ? never : T;
計算型
type Keys = keyof User; // 'id' | 'name' | 'email'
type UserValues = User[keyof User]; // string
拡張パターン
interfaceをinterfaceで拡張する
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
typeをtypeと交差させる
type Animal = { name: string };
type Dog = Animal & { breed: string };
組み合わせ(どちらの方法でも可能)
// Interface extends type
interface Dog extends Animal { breed: string; }
// Type extends interface
type Cat = Animal & { meows: boolean };
圧力に負けないためのプロトコル
1. 「どちらかを選んで、それを使い続ければいい」
圧力:「選択よりも一貫性のほうが重要です」
**回答:**そのとおりですが、なぜそれを選ぶのかを理解しましょう。
**対応:**オブジェクトにはinterfaceを、それ以外にはtypeを基本として使います。
2. 「インターフェースのユニオンが必要です」
圧力:「自分のインターフェースをユニオンにしたいです」
**回答:**型エイリアスを使えば、インターフェースをユニオンにできます。
対応:type Either = InterfaceA | InterfaceB;
要注意事項 - 立ち止まって再検討する
- タプル型に
interfaceを使う - 宣言マージが必要なのに
typeを使う - コードベース内の類似した型で使い方に一貫性がない
よくある正当化(すべて誤り)
| 言い訳 | 実際のところ |
|---|---|
| 「まったく同じです」 | いいえ。宣言マージとユニオン型に違いがあります |
| 「常にtypeのほうが優れています」 | interfaceのほうがエラーがわかりやすく、宣言マージもサポートします |
| 「常にinterfaceのほうが優れています」 | ユニオン型、タプル、マップ型は表現できません |
クイックリファレンス
// USE INTERFACE FOR:
interface User { name: string; } // Object types
interface Config extends Base { } // Extendable types
// USE TYPE FOR:
type ID = string | number; // Unions
type Point = [number, number]; // Tuples
type Handler = () => void; // Functions
type Keys = keyof User; // Computed types
type Mapped = { [K in Keys]: boolean }; // Mapped types
要点
オブジェクト型にはinterfaceを、それ以外にはtypeを使います。
インターフェースは宣言マージをサポートし、よりわかりやすいエラーメッセージを出力します。ユニオン、タプル、複雑な型操作には型エイリアスが必要です。コードベース内で一貫性を保ちましょう。
参考資料
Dan Vanderkam著『Effective TypeScript』のItem 13「Know the Differences Between type and interface」に基づいています。