TypeScriptのInterfaceとTypeの違いについて

静的型付け言語であるTypeScriptには、型を定義する構文として interfacetype という二つのキーワードがある。
これらはコンパイル時の型チェックのために存在し、JavaScriptへのコンパイル後には消去される。

オブジェクトの形で型を定義するという用途において両者は、同等のものを生成できる。そのためどちらを使うべきか迷う場面もあるだろう。

// interface で定義する場合
interface User {
  name: string
  age: number
}

// type で定義する場合
type User = {
  name: string
  age: number
}

それぞれの構文を見ると一見同じように見えるが、両者にはいくつかの明確な違いがある。 本記事ではその違いを軽く整理してみようと思う。

1. 継承の方法が異なる

interfaceextends キーワードで他のinterfaceを継承できるのに対し、type&(交差型) を使って型を合成する。 結果として得られる型は似ているが、構文としてのアプローチが異なる。

// interface: extends による継承
interface Animal {
  name: string
}

interface Dog extends Animal {
  breed: string
}

// type: & による合成
type Animal = {
  name: string
}

type Dog = Animal & {
  breed: string
}

interfaceextends は、親から子へと血筋を引き継ぐように型を受け継ぐ感じだ(「犬は動物の一種である」という is-a の関係)。
一方 type& は、既存の型同士を粘土のように混ぜ合わせて新しい型を作る。
どちらも最終的な形は同じだが、「継承」なのか「合成」なのかという思想の違いがある。

2. 宣言のマージに関して

interface は同名で複数回宣言すると自動的にマージされる。
これはライブラリの型定義を拡張したい場合などに便利だが、意図せず型が変わってしまうリスクもある。 一方 type は同名で再宣言するとコンパイルエラーになるため、こうした問題は起きない。

// interface: 同名宣言がマージされる
interface Config {
  debug: boolean
}

interface Config {
  verbose: boolean
}

// 結果: Config は { debug: boolean; verbose: boolean } になる

// type: 同名で再宣言するとエラー
type Config = {
  debug: boolean
}

type Config = {  // ❌ Error: Duplicate identifier 'Config'
  verbose: boolean
}

interface のマージは、共有のホワイトボードに誰でも付箋を追加できる状態に似ている。
チームで自由に情報を書き足せるのは便利だが、知らない間に誰かが書き足していて「あれ、こんなんあったっけ?」となるリスクがある。
type は、一度書いたらロックされるので変更したければ明示的に新しいバージョンを作る必要がある。
安全性と柔軟性のトレードオフだ。

3. 表現できる型の範囲にも差がある

interface はオブジェクトの形状や classが実装する契約interface で事前設計したものを classimplementsキーワード を用いて従わせる。) を定義するのに適しているが、ユニオン型やタプル型などを直接表現することはできない。
type はこれらを含むあらゆる型に別名を付けられるため、より柔軟な型表現が可能だ。

// type ならではの表現
type Status = "loading" | "success" | "error"    // ユニオン型
type Coordinate = [number, number]                // タプル型
type Callback = (value: string) => void           // 関数型

// interface ではこれらを直接表現できない
// interface Status = "loading" | "success" | "error"  ❌

interface は「設計図」に近い。
オブジェクトが持つべきプロパティやメソッドの形を定義する。

一方 type は「ラベル」に近い。
どんなものにも名前を付けられる。瓶の中身が液体でも粉でも混合物でも、ラベルを貼れる。
だからユニオン型のような「AかBかCのどれか」という抽象的な概念にも名前を付けることができる。

最後に使い分け指針に触れておく。
まずそもそもユニオン型やタプル型など、オブジェクト以外の型を表現するならtypeを使う。
ここに選択の余地はない。

一方、オブジェクトの形状を定義する場面では interfacetype のどちらでも書けるが、宣言マージによる拡張性やエラーメッセージの読みやすさから interfaceを使うようにする。(これらの点で公式ドキュメントでは interface の使用が推奨されている)

こんな感じ🍜