TypeScript で type と interface どっち使うのか問題

4 min read読了の目安(約3800字 4

はじめに

あくまで一個人の意見なので絶対的な解ではないというのと、どっちをデフォルトに選んでも普通にアプリケーション開発してて困ることはほぼほぼないと思うので、そこまで気を揉むことでもない、ということだけ最初に述べておいて意見をしたためます。

TL;DR

  • アプリケーション開発では基本的に type でおk
    • Declaration merging したい時だけ interface
  • ライブラリ開発のような使う側で拡張したい(Declaration merging したい)時は interface
  • とりあえずチームでどっちをデフォルトにするかは統一しといた方が気持ちいい

type と interface の違い

機能的にはそんなに大きな違いはなく、個人的に判断に関わるのは次の3つかなと思います。

  1. interface では Declaration merging がされる。type ではされない
  2. type では union 型や tuple 型が作れる。interface では作れない
  3. interface で定義したものは index signature で定義したオブジェクトに代入できない

1.interface では Declaration merging がされる。type ではされない

そもそも Declaration merging ってなんやねんという方のために説明すると、ざっくり言うと同じ名前の型(interface)定義が複数ある時に自動的に一つの定義にマージしてくれる機能のことです。

interface Person {
  name: string
}

interface Person {
  email: string
}

// name と email 両方必要になる
const john: Person = {
  name: "john",
  email: "hoge@example.com"
}

外部のライブラリの型なんかを名前を変えないまま拡張したい時などに便利な機能です。
ただデメリットとして、たまたまグローバルに定義されている型と同じ名前をつけてしまい拡張してしまうことが挙げられます。

このデメリットそんなに発動する機会ないのですが、Declaration merging が必要になる機会もほぼないと思うので、普段は type 使う方に倒しといた方がとりあえず safe なんじゃないのというのが個人的な意見です。

2.type では union 型や tuple 型が作れる。interface では作れない

こういったものです。

// Union
type Hoge = Fuga | Piyo

// tuple
type Hoge = [number, string]

こういうのを表現したい時は type を使うしかないです。

3.interface で定義したものは index signature で定義したオブジェクトに代入できない

そもそも index signature とはなんぞやという話ですが、次のようにオブジェクトの構造が規則に則っていることを規定できる型です。

// https://typescript-jp.gitbook.io/deep-dive/type-system/index-signatures から引用
let foo:{ [index:string] : {message: string} } = {};

/**
 * Must store stuff that conforms to the structure
 */
/** Ok */
foo['a'] = { message: 'some message' };
/** Error: must contain a `message` or type string. You have a typo in `message` */
foo['a'] = { messages: 'some message' };

ここで問題なのが、interface で定義したものは index signature を用いた型に代入できないということです。

一例を挙げてみると次のような {[key: string]: string} を引数を取るものがあるとして

declare function test(obj: {[key: string]: string}): void;

上記の obj に対応したものを、 type と interface それぞれで定義して代入してみようと思います。

type だと問題なくコンパイルできます。

type TestObjectType = {
  text: string,
}

const testObject: TestObjectType = {
  text: "mozi"
};

test(testObject) // 問題なし

ですが interface だと Index signature is missing in type 'TestObjectInterface'.ts(2345) というエラーが出ます。

interface TestObjectInterface {
  text: string,
}

const testObject: TestObjectInterface = {
  text: "mozi"
};

// Index signature is missing in type 'TestObjectInterface'.ts(2345)
test(testObject)

interface の場合は先ほど紹介した Declaration merging を待つため、型の中身が確定していません。今見えてる範囲では {[key: string]: string} を満たしていても、拡張されて number のプロパティが追加されるかもしれません。

あまり頻度は高くないかもしれないですが、これも interface を使う一つのデメリットなのかなと思います。

コメントを受けての追記

というのを interface のデメリットとして挙げましたが、どうも interface の挙動の方がより堅牢で私の解釈が間違っていたようです。

詳しくはこの記事の acomagu さんのコメントを参照いただければと思いますが、 index signature の型のオブジェクトに対して type で定義したオブジェクトでは index signature で定義した型でないプロパティも代入できてしまうようです。

https://zenn.dev/seya/articles/aa94166c977280#comment-9ec4de4f5c65a9

まとめ

以上から type に統一しておいた方が困ることは少なくなるのではと思っているので、個人的には type をデフォルト推しです。

参考にした記事・動画