- 技術 💻
TypeScriptのtypeとinterfaceの違い|迷ったときの判断基準
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期連続の黒字決算。