🆚

[TypeScript] interface vs type(とそれぞれの違いについて)

2024/07/07に公開

はじめに

5000兆回議論されている内容ですが、 TypeScript における interface と type どちらかを使うかについて、「なんとなくどっちかに合わせる」くらいの認識だったので改めて自分なりの答えを出すために調べ直しました。(なお、答えはありません。あくまで私の感想です 🙇)

調べた結果自分なりの答えは「理由がなければ type を使い、クラスを使う場合(OOP の場合)は interface を使うほうが都合が良い」ですが、プロジェクトの方針に則るのが最優先だと思います。

プラクティス

TypeScript Deep Diveサバイバル TypeScriptにも同じ話題があり、それぞれ以下のように記載されています。

TypeScript Deep Dive: type vs interface

https://typescript-jp.gitbook.io/deep-dive/styleguide#type-vs-interface

  • ユニオン型や交差型が必要な場合にはtypeを使い
  • extendimplementsをしたいときはinterfaceを使い
  • そうでなければ、その日あなたを幸せにするものを使用してください

サバイバル TypeScript: インターフェースと型エイリアスの使い分け

https://typescriptbook.jp/reference/object-oriented/interface/interface-vs-type-alias#インターフェースと型エイリアスの使い分け

  • 明確な正解はない
  • Google が公開している TypeScriptのスタイルガイド では、プリミティブな値やユニオン型やタプルの型定義をする場合は型エイリアスを利用し、オブジェクトの型を定義する場合はインターフェースを使うことを推奨している
  • 使い分けに悩む場合は型エイリアスに統一する考え方もある

interface と type の違い

違いの一覧

機能 type interface
宣言のマージ(Declaration merging) ❌ 使えない ✅ 使える
拡張(extends) 🔶 制約あり(交差型 & を使用) ✅ 使える
プリミティブ型のエイリアス ✅ 使える ❌ 使えない
ユニオン型 ✅ 使える ❌ 直接は使えない
タプル型 ✅ 使える 🔶 制約あり(インデックスシグネチャで表現)
オブジェクト型 ✅ 使える ✅ 使える
関数型 ✅ 使える ✅ 使える
リテラル型 ✅ 使える ❌ 直接は使えない
条件型 ✅ 使える ❌ 使えない
型アサーション ✅ 使える ❌ 使えない
infer キーワード ✅ 使える ❌ 使えない
typeof 演算子 ✅ 使える ❌ 使えない
keyof 演算子 ✅ 使える ❌ 使えない
インデックスシグネチャ ✅ 使える ✅ 使える
マッピング型 ✅ 使える ❌ 直接は使えない
テンプレートリテラル型 ✅ 使える ❌ 使えない
再帰的な型 ✅ 使える ✅ 使える

その他、interface では大規模プロジェクトにおいてコンパイル時のパフォーマンスが若干有利とされています。

また、エラーメッセージがわかりやすく宣言のマージや拡張が可能なため、ライブラリや公開APIの設計に適しているとされています。

詳細比較

宣言のマージ(Declaration merging)

https://typescriptbook.jp/reference/object-oriented/interface/open-ended-and-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;
}

拡張性

https://typescript-jp.gitbook.io/deep-dive/type-system#jiao-chai-xing-intersection-type

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 ではオブジェクトのプロパティとして定義します。
https://typescriptbook.jp/reference/values-types-variables/primitive-types

type
type SampleType = string;
interface
interface SampleType {
  value: string;
}

配列型

type は直接定義可能ですが プリミティブ型と同様に interface ではオブジェクトのプロパティとして定義します。
https://typescriptbook.jp/reference/values-types-variables/array/array-literal

type
type SampleTypeArray = string[];
interface
interface SampleTypeArray {
  values: string[];
}

タプル型

type では直接定義できますが、interface ではインデックスシグネチャを使って表現します。
https://typescriptbook.jp/reference/values-types-variables/tuple

type
type SampleTypeTuple = [string, number];
interface
interface SampleTypeTuple {
  0: string;
  1: number;
  length: 2; // タプルの長さを定義
}

オブジェクト型

type も interface も同様に定義可能です。
https://typescriptbook.jp/reference/values-types-variables/object/optional-property

type
type SampleTypeObject = {
  name: string;
  age: number;
};
interface
interface SampleTypeObject {
  name: string;
  age: number;
}

インデックスシグネチャ型

type も interface も同様に定義可能です。
https://typescriptbook.jp/reference/values-types-variables/object/index-signature

type
type SampleTypeIndex = {
  [key: string]: string;
};
interface
interface SampleTypeIndex {
  [key: string]: string;
}

関数型

type も interface も同様に定義可能です。
https://typescriptbook.jp/reference/functions/function-type-declaration

type
type SampleTypeFunction = (name: string) => string;
interface
interface SampleTypeFunction {
  (name: string): string;
}

ユニオン型

type は直接定義可能ですが プリミティブ型と同様に interface ではオブジェクトのプロパティとして定義します。
https://typescriptbook.jp/reference/values-types-variables/union

type
type SampleTypeUnion = string | number;
interface
interface SampleTypeArray {
  values: string | number;
}

交差型(インターセクション型)

type では直接指定できますが、interface では extends を用いて交差型と同様の型を定義します。
https://typescriptbook.jp/reference/values-types-variables/intersection

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 ではオブジェクトのプロパティとして定義します。
https://typescriptbook.jp/reference/values-types-variables/literal-types

type
type SampleTypeLiteral = "left" | "right" | "up" | "down";
interface
interface SampleTypeLiteral {
  direction: "left" | "right" | "up" | "down";
}

条件型

type では定義可能ですが interface では定義できません。
https://typescriptbook.jp/reference/type-reuse/conditional-types

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 では定義できません。
https://typescriptbook.jp/reference/values-types-variables/type-assertion-as

type
type SampleTypePerson = {
  name: string;
  age: number;
};
const person: SampleTypePerson = { name: "Alice", age: 30 } as SampleTypePerson;
interface
// interface では型アサーションは定義不可

infer キーワード

type では定義可能ですが interface では定義できません。
https://typescriptbook.jp/reference/type-reuse/infer

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 では定義できません。
https://typescriptbook.jp/reference/type-reuse/typeof-type-operator

type
const person = { name: "Alice", age: 30 };
type SampleTypeTypeof = typeof person;
interface
// interface では typeof 演算子は定義不可

keyof 演算子

type では定義可能ですが interface では定義できません。
https://typescriptbook.jp/reference/type-reuse/keyof-type-operator

type
type SampleTypeKeyof = {
  name: string;
  age: number;
};
type PersonKeys = keyof SampleTypeKeyof; // "name" | "age"
interface
// interface では keyof 演算子は定義不可

マッピング型

type では定義可能ですが interface では定義できません。
https://typescriptbook.jp/reference/type-reuse/mapped-types

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 では定義できません。
https://typescript-jp.gitbook.io/deep-dive/future-javascript/template-strings

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