TypeScriptのtypeとinterfaceは、どちらも型に名前を付ける書き方です。オブジェクトの形を表すだけなら、ほとんど同じように使えます。

type UserT = { id: number; name: string };
interface UserI { id: number; name: string }

違いが出るのは、拡張の仕方、同じ名前を2回宣言したとき、ユニオン型などのオブジェクト以外の型を扱うときです。この記事では、違いを実際にコンパイラー(tsc)で確認したエラーつきで比較したうえで、迷ったときにどちらを選べばよいかを判断表で示します。

typeとinterfaceの比較表

比較する点 type(型エイリアス) interface
オブジェクトの形 書ける 書ける
ユニオン型("a" | "b") 書ける 書けない
タプル・プリミティブの別名 書ける 書けない
マップ型({ [K in Keys]: ... }) 書ける 書けない
拡張 &(交差型)で組み合わせる extends で継承する
拡張で型が食い違ったとき エラーにならず、never になることがある 宣言した時点でエラーになる
同じ名前で2回宣言 エラー 自動で合体する(宣言のマージ)
Record<string, string> などへの代入 できる できない(インデックスシグネチャがないため)
classの implements できる(オブジェクト型なら) できる

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「TypeScriptのtypeとinterfaceの違い|迷ったときの判断基準」

違い1:拡張の仕方

interfaceは extends、typeは &(交差型)で拡張します。

interface Animal { name: string }
interface Dog extends Animal { breed: string }

type AnimalT = { name: string };
type DogT = AnimalT & { breed: string };

違いが出るのは、同じプロパティの型が食い違ったときです。

interface A { id: number }
interface B extends A { id: string }   // ここでエラー

type C = { id: number };
type D = C & { id: string };           // エラーにならない
const d: D = { id: 1 };                // 使うときに初めてエラー

実際のエラーは次のとおりです(tscで確認)。

error TS2430: Interface 'B' incorrectly extends interface 'A'.
  Types of property 'id' are incompatible.
    Type 'string' is not assignable to type 'number'.

error TS2322: Type 'number' is not assignable to type 'never'.

interfaceは宣言した場所で食い違いを教えてくれます。一方、typeの & は、id が number かつ string というありえない型(never)になり、使う場所で初めてエラーになります。エラーの場所がわかりにくくなるため、継承の関係をはっきりさせたいときはinterfaceの方が扱いやすい場面があります。

違い2:宣言のマージ

interfaceは、同じ名前で2回宣言すると、自動で1つに合体します。これを「宣言のマージ」と呼びます。

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

const u: User = { name: "佐藤" };   // エラー:ageがない
error TS2741: Property 'age' is missing in type '{ name: string; }'
  but required in type 'User'.

typeで同じことをすると、名前の重複としてエラーになります。

type T1 = { name: string };
type T1 = { age: number };   // error TS2300: Duplicate identifier 'T1'.

宣言のマージが役立つ場面

ライブラリの型をあとから拡張するときに使われます。たとえば、Expressのリクエストにログイン中のユーザーを追加する場合です。

declare global {
  namespace Express {
    interface Request {
      user?: { id: string; role: string };
    }
  }
}

意図しないマージに注意

一方で、意図せず合体してしまうこともあります。次のコードは、import や export がないファイル(グローバルなスクリプト)に書いた例です。

interface Comment { id: number; body: string }
const c: Comment = { id: 1, body: "こんにちは" };

一見問題なさそうですが、ブラウザ向けの型(lib.dom)を読み込んでいると、次のエラーになります(tscで確認)。

error TS2740: Type '{ id: number; body: string; }' is missing the following
  properties from type 'Comment': data, length, ownerDocument, appendData, and 59 more.

ブラウザの標準の型にも Comment(HTMLのコメントを表す型)があり、自分の Comment と合体してしまったのが原因です。同じ内容をtypeで書くと「名前が重複している」というエラーになり、原因にすぐ気づけます。

この問題は、ファイルに import か export を1つでも書いてモジュールにすると起きません。一般的なプロジェクトの構成であれば各ファイルはモジュールになっていますが、Comment・Event・Request・Response など標準の型と同じ名前を使うときは意識しておくと安心です。

違い3:オブジェクト以外の型はtypeだけ

ユニオン型、タプル、プリミティブの別名、マップ型、条件型などは、typeでしか書けません。

type Status = "draft" | "published" | "archived";   // ユニオン型
type Point = [number, number];                      // タプル
type UserId = string;                               // プリミティブの別名
type Flags = { [K in Status]: boolean };            // マップ型
type Result<T> = { ok: true; value: T } | { ok: false; error: string };

特に、Result<T> のような「成功か失敗か」で形が変わる型(判別可能なユニオン型)は、TypeScriptでよく使うパターンで、typeで書きます。

違い4:インデックスシグネチャへの代入

少しわかりにくい違いですが、実務で出会うことがあります。

interface IUser { name: string }
type TUser = { name: string };

const iu: IUser = { name: "a" };
const tu: TUser = { name: "a" };

const r1: Record<string, string> = tu;   // OK
const r2: Record<string, string> = iu;   // エラー
error TS2322: Type 'IUser' is not assignable to type 'Record<string, string>'.
  Index signature for type 'string' is missing in type 'IUser'.

interfaceは宣言のマージで後からプロパティが増える可能性があるため、「すべてのプロパティが文字列」とは言い切れず、代入が許されません。オブジェクトを Record<string, ...> として受け取る関数(ログ出力やクエリ文字列への変換など)に渡すときに、この違いが出ます。

迷ったときの判断表

質問 はい → いいえ →
ユニオン型・タプル・マップ型など、オブジェクト以外の型か type 次へ
ライブラリの型を拡張する(宣言のマージが必要)か interface 次へ
classで implements し、継承の関係を中心に設計するか interface が読みやすい 次へ
チームやプロジェクトの規約があるか 規約に従う 次へ
どちらでもよい チームでどちらかに統一する ―

どちらでもよい場面では、どちらを選ぶかより、プロジェクトの中で揃っていることの方が大切です。よく採用されている方針は、次の2つです。

方針 内容 向いているチーム
オブジェクトはinterface、それ以外はtype Googleのスタイルガイドなどで採用されている考え方 classを使う設計が多い、継承の関係を明確にしたい
基本はtype、必要なときだけinterface 書き方を1つに寄せ、宣言のマージを必要な場面だけに限る 関数とデータ中心の設計、ReactなどUIの開発

方針を決めたら、ESLintの @typescript-eslint/consistent-type-definitions ルールで自動でチェックすると、レビューで指摘し合う手間が省けます。

satisfiesとの組み合わせ

typeとinterfaceのどちらで定義した型でも、satisfies と組み合わせると、型のチェックを受けながら、値の具体的な型を保てます。

type Route = { path: string; auth: boolean };

const routes = {
  home:  { path: "/",      auth: false },
  admin: { path: "/admin", auth: true },
} satisfies Record<string, Route>;

routes.admin.path;    // OK
routes.nothing;       // エラー:存在しないキー

const routes: Record<string, Route> = ... と書くと、routes.nothing もエラーにならず(どんなキーでも受け付ける型になるため)、タイプミスに気づけません。satisfies なら、定義した形に合っているかを確認しつつ、実際にあるキーだけを使えるようになります。

よくある質問

Q. 性能(コンパイルの速さ)に違いはありますか? A. 大規模なプロジェクトで、交差型(&)を大量に組み合わせると、型チェックが重くなることがあると言われています。TypeScriptのチームのガイドでも、オブジェクトの拡張には extends の方が効率的とされています。通常の規模では、気にするほどの差は出にくいです。

Q. Reactのpropsはどちらで書けばいいですか? A. どちらでも書けます。ユニオン型を組み合わせることが多いため、typeで統一しているプロジェクトが多い印象ですが、チームの規約に合わせれば問題ありません。

Q. 途中でtypeからinterfaceに(または逆に)書き換えても大丈夫ですか? A. オブジェクトの形を表すだけなら、多くの場合そのまま書き換えられます。ただし、& と extends の違い(食い違いの扱い)や、宣言のマージの有無で挙動が変わることがあるため、書き換えた後は型チェック(tsc --noEmit)を実行して確認します。

まとめ

  • オブジェクトの形を表すだけなら、typeとinterfaceはほぼ同じ
  • ユニオン型・タプル・マップ型などはtypeだけで書ける
  • 拡張は、interfaceは extends(食い違いを宣言した場所で検出)、typeは &(never になることがある)
  • interfaceは同じ名前で合体する(宣言のマージ)。ライブラリの拡張に便利だが、Comment など標準の型との意図しない合体に注意
  • interfaceは Record<string, ...> に代入できない
  • 迷ったら、チームで方針を決めて統一し、ESLintで自動チェックする

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「TypeScriptのtypeとinterfaceの違い|迷ったときの判断基準」

株式会社RJCでは、型を活かした読みやすく安全なコードを、仲間とレビューし合いながら書いています。こうした知識を共有しながら、一緒に成長を楽しんでいます。

株式会社RJC ― SI事業・SES事業・AI駆動開発。未経験からエンジニアに育てるのが得意な会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?