[TypeScript] interface vs type(とそれぞれの違いについて)
はじめに
5000兆回議論されている内容ですが、 TypeScript における interface と type どちらかを使うかについて、「なんとなくどっちかに合わせる」くらいの認識だったので改めて自分なりの答えを出すために調べ直しました。(なお、答えはありません。あくまで私の感想です 🙇)
調べた結果自分なりの答えは「理由がなければ type を使い、クラスを使う場合(OOP の場合)は interface を使うほうが都合が良い」ですが、プロジェクトの方針に則るのが最優先だと思います。
プラクティス
TypeScript Deep Dive と サバイバル TypeScriptにも同じ話題があり、それぞれ以下のように記載されています。
type
vs interface
TypeScript Deep Dive:
- ユニオン型や交差型が必要な場合には
type
を使い -
extend
やimplements
をしたいときはinterface
を使い - そうでなければ、その日あなたを幸せにするものを使用してください
サバイバル TypeScript: インターフェースと型エイリアスの使い分け
- 明確な正解はない
- Google が公開している TypeScriptのスタイルガイド では、プリミティブな値やユニオン型やタプルの型定義をする場合は型エイリアスを利用し、オブジェクトの型を定義する場合はインターフェースを使うことを推奨している
- 使い分けに悩む場合は型エイリアスに統一する考え方もある
interface と type の違い
違いの一覧
機能 | type | interface |
---|---|---|
宣言のマージ(Declaration merging) | ❌ 使えない | ✅ 使える |
拡張(extends) | 🔶 制約あり(交差型 & を使用) |
✅ 使える |
プリミティブ型のエイリアス | ✅ 使える | ❌ 使えない |
ユニオン型 | ✅ 使える | ❌ 直接は使えない |
タプル型 | ✅ 使える | 🔶 制約あり(インデックスシグネチャで表現) |
オブジェクト型 | ✅ 使える | ✅ 使える |
関数型 | ✅ 使える | ✅ 使える |
リテラル型 | ✅ 使える | ❌ 直接は使えない |
条件型 | ✅ 使える | ❌ 使えない |
型アサーション | ✅ 使える | ❌ 使えない |
infer キーワード |
✅ 使える | ❌ 使えない |
typeof 演算子 |
✅ 使える | ❌ 使えない |
keyof 演算子 |
✅ 使える | ❌ 使えない |
インデックスシグネチャ | ✅ 使える | ✅ 使える |
マッピング型 | ✅ 使える | ❌ 直接は使えない |
テンプレートリテラル型 | ✅ 使える | ❌ 使えない |
再帰的な型 | ✅ 使える | ✅ 使える |
その他、interface では大規模プロジェクトにおいてコンパイル時のパフォーマンスが若干有利とされています。
また、エラーメッセージがわかりやすく宣言のマージや拡張が可能なため、ライブラリや公開APIの設計に適しているとされています。
詳細比較
宣言のマージ(Declaration merging)
interface では同名の interface を複数回宣言するとそれらの定義がマージされますが、type では同名の type を再宣言しようとするとエラーになります。
OPP(オブジェクト指向プログラミング)では interface の拡張や override 等をするときに便利な概念ですが、React 等で使われる FP(関数型プログラミング)においては Immutability(不変性)の概念に反するので、うっかり同じ名前の型を定義してしまわないよう注意が必要です。
interface
interface Person {
name: string;
}
interface Person {
age: number;
}
// 結果:
// interface Person {
// name: string;
// age: number;
// }
type
type Person = {
name: string;
}
// エラー: 重複した識別子 'Person'
type Person = {
age: number;
}
拡張性
interface ではextends
キーワードを使用して他のインターフェースを拡張しますが、type では交差型(&
)を使用して型を拡張します。
また、宣言マージでは新しく定義した型は1つの型に統合されますが、extends では新しい型を定義します。
interface
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// 結果:
// interface Animal {
// name: string;
// breed: string;
// }
type
type Animal = {
name: string;
}
type Dog = Animal & {
breed: string;
}
// 結果:
// type Dog = {
// name: string;
// breed: string;
// }
それぞれの type の指定方法の違い
プリミティブ型
type は直接定義可能ですが、interface ではオブジェクトのプロパティとして定義します。
type
type SampleType = string;
interface
interface SampleType {
value: string;
}
配列型
type は直接定義可能ですが プリミティブ型と同様に interface ではオブジェクトのプロパティとして定義します。
type
type SampleTypeArray = string[];
interface
interface SampleTypeArray {
values: string[];
}
タプル型
type では直接定義できますが、interface ではインデックスシグネチャを使って表現します。
type
type SampleTypeTuple = [string, number];
interface
interface SampleTypeTuple {
0: string;
1: number;
length: 2; // タプルの長さを定義
}
オブジェクト型
type も interface も同様に定義可能です。
type
type SampleTypeObject = {
name: string;
age: number;
};
interface
interface SampleTypeObject {
name: string;
age: number;
}
インデックスシグネチャ型
type も interface も同様に定義可能です。
type
type SampleTypeIndex = {
[key: string]: string;
};
interface
interface SampleTypeIndex {
[key: string]: string;
}
関数型
type も interface も同様に定義可能です。
type
type SampleTypeFunction = (name: string) => string;
interface
interface SampleTypeFunction {
(name: string): string;
}
ユニオン型
type は直接定義可能ですが プリミティブ型と同様に interface ではオブジェクトのプロパティとして定義します。
type
type SampleTypeUnion = string | number;
interface
interface SampleTypeArray {
values: string | number;
}
交差型(インターセクション型)
type では直接指定できますが、interface では extends を用いて交差型と同様の型を定義します。
type
type SampleTypeName = { name: string };
type SampleTypeAge = { age: number };
type SampleTypeIntersection = SampleTypeName & SampleTypeAge;
interface
interface SampleTypeName {
name: string;
}
interface SampleTypeAge {
age: number;
}
interface SampleTypeIntersection extends SampleTypeName, SampleTypeAge {}
リテラル型
type は直接定義可能ですが プリミティブ型と同様に interface ではオブジェクトのプロパティとして定義します。
type
type SampleTypeLiteral = "left" | "right" | "up" | "down";
interface
interface SampleTypeLiteral {
direction: "left" | "right" | "up" | "down";
}
条件型
type では定義可能ですが interface では定義できません。
type
type SampleTypeCondition<T> = T extends string ? "string" : "not string";
type Test1 = SampleTypeCondition<string>; // "string"
type Test2 = SampleTypeCondition<number>; // "not string"
interface
// interface では条件型は定義不可
型アサーション
type では定義可能ですが interface では定義できません。
type
type SampleTypePerson = {
name: string;
age: number;
};
const person: SampleTypePerson = { name: "Alice", age: 30 } as SampleTypePerson;
interface
// interface では型アサーションは定義不可
infer
キーワード
type では定義可能ですが interface では定義できません。
type
type SampleTypeInfer<T> = T extends (...args: any[]) => infer R ? R : never;
type TestFunction = () => string;
type ReturnType = SampleTypeInfer<TestFunction>; // string
interface
// interface では infer キーワードは定義不可
typeof
演算子
type では定義可能ですが interface では定義できません。
type
const person = { name: "Alice", age: 30 };
type SampleTypeTypeof = typeof person;
interface
// interface では typeof 演算子は定義不可
keyof
演算子
type では定義可能ですが interface では定義できません。
type
type SampleTypeKeyof = {
name: string;
age: number;
};
type PersonKeys = keyof SampleTypeKeyof; // "name" | "age"
interface
// interface では keyof 演算子は定義不可
マッピング型
type では定義可能ですが interface では定義できません。
type
type SampleTypeMapping<T> = {
readonly [P in keyof T]: T[P];
};
type ReadOnlyPerson = SampleTypeMapping<SampleTypeObject>;
interface
// interface ではマッピング型は定義不可
ただし、以下のように type を経由することで定義することができます
interface SampleTypeObject {
name: string;
age: number;
}
// type を使用してマッピング型を適用
type SampleTypeMapping<T> = {
readonly [P in keyof T]: T[P];
};
// interface で ReadOnlyPerson を拡張
// type を使用してマッピング型を適用
type ReadOnlyPerson = SampleTypeMapping<SampleTypeObject>;
// interface で ReadOnlyPerson を拡張
interface Employee extends ReadOnlyPerson {
employeeId: number;
}
// Employee の型:
// {
// readonly name: string;
// readonly age: number;
// employeeId: number;
// }
テンプレートリテラル型
type では定義可能ですが interface では定義できません。
type
type SampleTypeTemplate<T extends string> = `Hello, ${T}`;
type GreetAlice = SampleTypeTemplate<"Alice">; // "Hello, Alice"
interface
// interface ではテンプレートリテラル型は定義不可
再帰型
type も interface も同様に定義可能です。
type
type SampleTypeRecursive<T> = T | SampleTypeRecursive<T>[];
const numbers: SampleTypeRecursive<number> = [1, [2, [3, 4], 5]];
interface
interface SampleTypeRecursive {
value: number | SampleTypeRecursive[];
}
const nested: SampleTypeRecursive = {
value: [1, { value: [2, 3] }]
};
おわりに(所感)
実は今まで脳死で type を使っていましたが、改めて調べてみて以下のような感想になりました 🙋
- フロント、特に React では FP(関数型プログラミング)の思想が強いため、不意に型を拡張できてしまう interface の振る舞いは少し違和感があった
- FP では Immutability(不変性)が重視されるため、宣言マージが発生するとこれに反する
- interface は class と組み合わせた時に一番能力を発揮するように感じた
- ただ、最近のトレンド(特にフロント)では class を使うことが少ないので interface であるメリットは少ないように感じた
なので私個人の結論としては「理由がなければ type を使い、クラスを使う場合(OOP の場合)は interface を使うほうが都合が良い」になります。
主観ですが、最近では node.js などのバックエンドでも FP が主流になってきているようなので、interface を使う機会は少ないかもしれません 🤔
Discussion