この記事は ミライトデザイン Advent Calendar 2025 の 15 日目の記事です。
前回は hoge さんの 「わからない」にぶち当たったときの分解チートシートという記事でした。
記事作成時点ではまだ読めていないのですが、日々「わからない」に直面するエンジニアとして、とても気になるテーマです。
はじめに
TypeScript を3年ほど使っていますが、これまで型定義はほぼ type だけで書いてきました。
interface も存在は知っていましたが、参加しているプロジェクトでほとんど使われておらず、正直なところ両者の違いをちゃんと理解していませんでした。
この記事では、TypeScript 公式ドキュメントを参考にしながら、type と interface の違いを整理し、どう考えて使えばよさそうかをまとめてみます。
type の性質
これまで自分がよく使ってきたのが type です。
型定義については、基本的にはこれで困っていませんでした。
type User = {
id: number;
name: string;
};
type は「型エイリアス」
type は正式には 型エイリアス(Type Alias) と呼ばれています。
エイリアスという名前の通り、type は新しい型を作るものではなく、既存の型に別名を付ける仕組みです。
type UserId = number;
この場合、UserId は「ユーザーID」という意図を表すための名前ではありますが、型としては number と区別されるわけではありません。
名前は違っても、型としては区別されない
type はあくまで既存の型に別名を付けているだけなので、名前が違っていても型としては同じものとして扱われます。
「型として同じ扱いになる」とはどういうことか、簡単な例で確認してみます。
type UserId = number;
type ProductId = number;
let userId: UserId = 1;
let productId: ProductId = 2;
userId = productId; // OK
UserId と ProductId は意味的には別のものですが、TypeScript ではどちらも number として扱われるため、代入してもエラーにはなりません。
再定義できない
type は型エイリアスであり、既存の型に名前を付ける仕組みでした。
そのため、type では同じ名前で型の定義を行うことはできません。
type User = {
id: number;
};
type User = {
name: string;
};
// Error: Duplicate identifier 'User'
このように、同じ名前の type をもう一度定義しようとするとエラーになります。
type が向いている場面
type は複数の型やパターンをひとつの型として表したい場面で便利です。
プリミティブ型に意味のある名前を付ける
type UserId = number;
複数の型を組み合わせて表現する
type User = {
id: number;
};
type Profile = {
name: string;
};
type UserProfile = User & Profile;
union 型で状態を表す
type Status = "success" | "error" | "loading";
このように、
- プリミティブ型に意味を持たせたいとき
- 既存の型を組み合わせたいとき
- 状態やパターンを列挙したいとき
といった場面では、type は使うケースが多いと思います。
interface は何が違うのか
ここまで type を整理してきました。
次は interface について見ていきます。
interface は「オブジェクトの形」を表すもの
interface は、オブジェクトがどんなプロパティを持っているかを表すための仕組みです。
interface User {
id: number;
name: string;
}
これは「User というオブジェクトは、id と name を持っている」ということを表しています。
ここで大事なのは、TypeScript が見ているのは型の名前ではなく、オブジェクトの中身(構造)だけという点です。
const userData = {
id: 1,
name: "Alice",
};
const user: User = userData; // OK
この userData は、先ほど User という interface で定義した構造と結果的に同じ形をしています。
TypeScript は「User という名前かどうか」ではなく、必要なプロパティを持っているかだけをチェックします。
同じ形なら、同じように扱われる
例えば、次のようなコードも問題なく動きます。
interface User {
id: number;
name: string;
}
function printUser(user: User) {
console.log(user.name);
}
printUser({
id: 1,
name: "Bob",
});
この例では、User 型の変数を経由せず、オブジェクトを直接 printUser に渡していますが、User の形を満たしているため、そのまま使えています。
このように、形が一致していれば同じ型として扱われるという考え方を、TypeScript では「構造的型付け」と呼びます。
再宣言できる
interface のもうひとつの大きな特徴が、同じ名前で再宣言できることです。
interface User {
id: number;
}
interface User {
name: string;
}
この場合、エラーにはならず、最終的に User は次のような形として扱われます。
{
id: number;
name: string;
}
type ではできなかったこの挙動から、interface は後から拡張されることを想定して設計されていることがわかります。
extends で構造を広げられる
interface は extends を使って、既存の構造をそのまま引き継ぐこともできます。
interface User {
id: number;
name: string;
}
interface Admin extends User {
role: string;
}
この場合、Admin は
-
Userの性質(idとname)を持ちつつ - さらに
roleを追加したもの
を表しています。
「Admin は User を元に作られている」という関係がコードから自然に読み取れます。
拡張の柔軟性と注意点
interface は再宣言や extends によって後から拡張できるのが特徴ですが、この柔軟性には注意点もあると感じました。
コード量が多いプロジェクトでは、知らないところで同じ名前の interface が宣言され、意図せず拡張されてしまう可能性があります。
// user.ts
interface User {
id: number;
name: string;
}
// admin.ts
interface User {
email: string;
}
// main.ts
const user: User = {
id: 1,
name: "Alice",
email: "alice@example.com" // emailも必須になっている
};
この例では、admin.ts 側で同じ名前の User が再宣言されているため User インターフェースがマージされ、email プロパティも必須になっています。
このような予期しない拡張を避けたい場合は、type を使う方が安全です。
type は再定義するとエラーになるため、意図しない変更を防げます。
ここまで整理すると、interface は
- オブジェクトの形を表すためのもの
- 名前よりも構造が重視される
- 後から拡張されることを前提としている
という性質を持っています。
type と interface をどう考えるか
ここまで整理してきて、type と interface は「どちらが正しいか」を比べるものではなく、それぞれ前提として考えている使い方が少し違うだけだと感じました。
type は「型を組み立てる」ためのもの
type は、既存の型を組み合わせたり、意味のある名前を付けたりする用途でよく使われます。
type Status = "success" | "error" | "loading";
このように、複数のパターンをひとつの型としてまとめたいときは、type を使うのが良さそうです。
一方で、type は再定義できず、後から形が変わることを前提とした使い方には向いていません。
interface は「オブジェクトの契約」を表すもの
interface は、オブジェクトがどんな構造を持っているかを表すための仕組みです。
interface User {
id: number;
name: string;
}
次のような場面では interface の方が適していそうです。
- オブジェクトの形を明確にしたい
- 後から拡張される可能性がある
- 「この形を満たしてほしい」という意図を伝えたい
まとめ
今回あらためて整理してみて、
- 「なぜ
typeは再定義できないのか」 - 「なぜ
interfaceは宣言をマージできるのか」
といった背景を知ること自体に意味があると感じました。
正直、違いはなんとなく理解できたものの、すぐに明確な使い分けができるかというとまだ難しそうです。
ただ、「何となく使う」状態から「理解した上で選択する」状態には一歩近づけた気がしています。
今後も実際のコードを書きながら、どちらを選ぶかは模索していきたいと思います。
明日は takuma さんの記事です!
楽しみですね!
参考資料