atama plus techblog
🤔

typeとinterfaceって結局どう使い分ければ良いの?

2024/09/30に公開

TypeScriptではtype alias syntax(型エイリアス構文)とinterface declaration(インターフェース宣言)を使って型を定義できます。
おおよそ両者同じことができるので、どちらを使うか迷います。

両者の使い分けに関する記事は沢山あります。

https://zenn.dev/luvmini511/articles/6c6f69481c2d17

https://mosya.dev/blog/type-interface

これらの記事を読んで基本的にはtypeを使えば良いと思っていました。

ですが最近以下のことがあり、typeとinterfaceの使い分けがわからなくなってしまいました。

  • typeよりもinterfaceの方がコンパイルのパフォーマンスが良いという話を耳にした。
  • interfaceしか使えない特定の機能を知った。

そこでtypeとinterfaceの違いを学んで、どう使い分ければよいかを整理しました。

type, interfaceそれぞれのメリット

typeのメリット

interfaceで表現できないことが表現できる

typeでのみ表現可能でinterfaceには表現できないことがあります。

以下はtypeでのみ表現可能です。

type UnionTypes = 'type' | 'interface';
type UnionTypes = 'type' | 'interface';
type MappedTypes = {
  [key in UnionTypes]: string;
}
type ConditionalTypes<T> = T extends string ? true : false;

interfaceはこれらを表現できませんがtypeは表現できます。これらを表現したい場合はtype一択です。

エディタでホバーした時に型定義の中身が見える

typeは一般的なエディタでホバーした際に型定義の中身を見ることができます。(playground)(ただしunionは各オブジェクトの中身は展開されないです)

つど型定義に飛ばなくても中身が見られるので便利です。そのためこれまで私はtypeを好んで使っていました。

豆知識: 型定義を分かりやすくするPrettify

以下で定義するPrettifyを用いるとホバーした時に表示される型が分かりやすくなります。

type Prettify<T> = {
  [K in keyof T]: T[K];
} & {};

https://www.totaltypescript.com/concepts/the-prettify-helper

この型を使うとintersectionがマージされた状態で表示されたり、unionが展開されて表示されます。(playground)

type Name = {
  name: string;
};
type Age = {
  age: number;
}
type User = Name & Age

type PrettifiedUser = Prettify<User>
// type PrettifiedUser = {
//   name: string;
//   age: number
// }

またinterfaceでもこの型を使うとホバーした時に展開されます。

interface User extends Name, Age {}

type PrettifiedUser = Prettify<User>
// type PrettifiedUser = {
//   name: string;
//   age: number;
// }

このPrettifyを用いることでホバーした時に表示される型情報を分かりやすくできます。

この特性を用いて型情報を分かりやすく表示するVSCode拡張を作っている方がいました。

https://tech.mobilefactory.jp/entry/2021/12/02/000000

記事中の拡張機能を使うことでinterfaceもtypeと同じように型情報を即座に参照できます。そのため型情報の閲覧のしやすさという観点でtypeの優位性はそこまで高くありません。

interfaceのメリット

extendsがintersectionより速い

typeのintersectionまたはinterfaceのextendsを使うことで複数の型を結合できます。

type Name = {
  name: string;
};
type Age = {
  age: number;
}

// typeの場合
type TypeUser = Name & Age

// interfaceの場合
interface InterfaceUser extends Name, Age {}

このときtypeのintersectionよりinterfaceのextendsの方がコンパイルのパフォーマンスが良いそうです。(実行時のパフォーマンスではないです)

Microsoftのドキュメントではinterfaceのextendsがtypeのintersectionより推奨されています。

https://github.com/microsoft/TypeScript/wiki/Performance#preferring-interfaces-over-intersections

intersectionを使うとコンパイルのパフォーマンスが悪い具体例を書いている記事もあります。

https://www.totaltypescript.com/react-apps-ts-performance

よって複数の型を結合した複雑な型を定義する場合、interfaceを用いる方が良いそうです。

上書き(declaration merging)ができる

同じ名前のinterfaceを複数定義すると宣言がmergeされます。declaration mergingというテクニックらしいです。(playground)

interface User {
  name: string;
};
interface User {
  age: number;
}

type Prettify<T> = {
  [K in keyof T]: T[K];
} & {};

type PrettifiedUser = Prettify<User>
// {
//   name: string;
//   age: number;
// }

この性質を用いてライブラリのバージョンによる型の違いを簡単に表現できるそうです。

詳しくは以下の記事でわかりやすく解説されているのでご覧ください。
https://zenn.dev/uhyo/articles/typescript-lib-declaration-merging

またこの性質によりライブラリの型を利用者が自由に拡張可能です。
ライブラリの型定義を上書きし、使用して欲しくないプロパティを使用できないようにするといったことが手軽に行えます。

参考: https://typescriptbook.jp/reference/object-oriented/interface/interface-vs-type-alias#インターフェースの利用例

一方、typeではdeclaration mergingはできません。
そのため型の拡張性が必要な場合、typeよりもinterfaceが適していそうです。
実際、型定義の拡張性が求められるライブラリの型定義はinterfaceを用いることが多そうです。

プロパティのオーバーライド時に互換性がないとエラーになる

interfaceでプロパティをオーバーライドする場合、型の互換性がないとエラーになります。(playground)

interface User {
  name: string
}

interface ExtendedUser extends User {
  name: number
}
// Interface 'ExtendedUser' incorrectly extends interface 'User'.
//  Types of property 'name' are incompatible.
//    Type 'number' is not assignable to type 'string'.(2430)

この場合stringとnumberに互換性がないためコンパイルエラーになります。

typeを用いた場合、エラーにはならずneverになります。(playground)

type User = {
  name: string
}

type ExtendedUser = User & {
  name: number
}
// {
//    name: never; // string | number でneverになる
// }

interfaceだとコンパイルエラーになってくれるので実装ミスに気づきやすいです。

参考: https://typescriptbook.jp/reference/object-oriented/interface/interface-vs-type-alias#プロパティのオーバーライド

使い分けまとめ

type, interfaceそれぞれのメリットを踏まえて使い分けをまとめます。

interfaceを使う場合

コンパイルのパフォーマンスが気になる

typeのintersectionを用いて複雑な型を作成している場合コンパイルのパフォーマンスが悪くなります。その場合interfaceを用いることでコンパイルのパフォーマンスが改善する可能性があります。

型定義の拡張性が必要

ライブラリの型定義のように、バージョンごとに型定義を変えやすくしたり、ユーザーが型定義を拡張しやすくするためにはinterfaceが適しています。

オーバーライドのミスを防ぎたい

オーバーライド時に型互換性がないとinterfaceはコンパイルエラーを出してくれます。
それにより実装ミスに気づくことができます。

typeを使う場合

typeでしか定義できないものを定義したい

typeでしか定義できないものを定義する場合typeが必須になります。

  • Union Types
  • Mapped Types
  • Conditional Types

どちらでも良い場合

上記以外はtypeでもinterfaceでもどちらでも表現可能で優劣はなさそうなので好きな方を使うのが良いと思います。

公式ハンドブックでは「好きな方を使っていいけど、目安が欲しかったらtypeの機能が必要になるまでinterfaceを使っておくとよいよ」と書いてありました!

For the most part, you can choose based on personal preference, and TypeScript will tell you if it needs something to be the other kind of declaration. If you would like a heuristic, use interface until you need to use features from type.

好みがある方は好みのものを使用して、好みがないよという方はinterfaceを使うのが無難かもしれません。

参考記事

https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces

https://www.totaltypescript.com/type-vs-interface-which-should-you-use

https://typescriptbook.jp/reference/object-oriented/interface/interface-vs-type-alias

https://zenn.dev/luvmini511/articles/6c6f69481c2d17

https://mosya.dev/blog/type-interface

atama plus techblog
atama plus techblog

Discussion