💡

一日一処: TypeScriptのinterface vs type論争の個人的解釈と記述ルール(ESLint)について

2024/02/11に公開

interface vs type

これに関しては様々な解釈もあり、私が投稿する以前にも多くの記事が存在する。個人的には、沢山ある記事の技術的な考察も触れつつ、決定してほしい。公式ドキュメントにおいても判断に至る明確な基準はない

個人的な使い方

まずは、根源たるJavaScriptへ原点回帰しよう。JavaScriptでは、classを宣言することが、元々できなかった。バージョンの変化によって、現在はこれを用いる事が可能だが、JavaScript自体は、プロトタイプベースのオブジェクト指向スクリプト言語とされており、JavaやC#などのクラス定義とは若干異なる。全てはオブジェクトであり、クラスから生成され利用可能となったインスタンスは、もれなくオブジェクトとなる。

class Test {}
const test = new Test()
console.log(typeof test) // object

これ自体は、特に驚くことでもないし、この時点では、interfaceもtypeもどちらでもいいということにしかならない。
ただ、これを示したのには理由がある。インスタンス化されたすべての機能はオブジェクトであり、JSONでも活用されるいわゆる連想配列のようなものもオブジェクトで、これもObjectのインスタンスに該当するため、メソッドを有している全ては、オブジェクトであるという結論に至る。

class Test {}
const test = new Test()
console.log(typeof test) // object
const obj = { name: 'obj' }
console.log(typeof obj)  // object

余談だが、文字列においても、通常のリテラルでの宣言とStringクラスをインスタンス化した場合とでは、その実体が変わる。詳細は割愛するが、結局のところ、文字列だってオブジェクトなり得るということだ。

const test1 = new String('test')
console.log(test1)        // test
console.log(typeof test1) // object
const test2 = 'test'
console.log(test2)        // test
console.log(typeof test2) // string

1つ目の理由: クラスからみたinterface

通常、classの型を定義する場合は、extendsしていること(interfaceも可能)も考え、interfaceを用いるのが一般的だ。また、interfaceを利用できる他言語でも同様だ。TypeScriptだけそれ以外を用いる理由はない。

interface IUser {
  name: string,
  say(message: string): void
}

class User implements IUser {
  name = 'bob'
  say(message: string): void {
    console.log(message)
  }
}

なぜ、「TypeScriptだけそれ以外を用いる」という表現をしたかというと、面白いことに、このような表現だって可能だ。

type IUser = {
  name: string,
  say(message: string): void
}

class User implements IUser {
  name = 'bob'
  say(message: string): void {
    console.log(message)
  }
}

interfaceで有ることを明示する慣例として、先頭にIさえつけておけば、どこでも理解し利用できるし、定義そのものは、interfaceなのかtypeなのかはさほど重要ではない。
ただし、継承を行うclassという存在にとって、型そのものも、その振る舞いに寄せたいし、定義を見たときに瞬間的に理解したい。そのため、クラスの型定義では、interfaceを用いる。

2つ目の理由: 最終的にオブジェクトになるのなら

2つ目の理由は、繰り返しとなるが、1つ目の理由を鑑みると、このクラスは、インスタンス化するとオブジェクトになる。interfaceを元に実装したclassをインスタンス化したらobjectとなる、これを逆に言えば、objectの元をたどればinterfaceとなることになる。そして、object自体、項目が状況によって要素が増えることもあり、それを原点に戻って考えると、継承しているということになる。むしろ、その様に考えたほうが自然だ。
言いたいことをコードで表すとこの様になる。

interface IUser {
    name: string,
}

interface IEmployee extends IUser {
    job: string,
}

const data: IEmployee = {
    name: 'bob',
    job: 'Engineer'
}

class Employee implements IEmployee {
    name = 'bob'
    job = 'Engineer'
}

この通り、dataは常にオブジェクトであることはわかるし、Employeeもインスタンス化してしまえばオブジェクトになる。そして、この内容の2つにどのような差があるのか。どちらも機能上は同一だ。何の違いも存在しない。言うならば、クラス定義のほうが、機能を拡張できる余地があるという程度だ。ただ、これに至っては、dataも同様のことがいえる。

interface IUser {
    name: string,
    greet: () => void
}

interface IEmployee extends IUser {
    job: string,
}

const data: IEmployee = {
    name: 'bob',
    job: 'Engineer',
    greet: function() {
        console.log(`Hello, I'm ${this.name}`)
    }
}

class Employee implements IEmployee {
    name = 'bob'
    job = 'Engineer'
    greet() {
        console.log(`Hello, I'm ${this.name}`)
    }
}

interfaceにgreetメソッドを追加した。そして、両方ともほぼ同じコードを追加した。これを見ると分かり易いが、JavaScriptのクラス定義は、正確には異なる部分もあるが単純な例の場合においては、もとよりあったオブジェクト定義のエイリアスに過ぎない。これを見ると、オブジェクトでの型がtypeではなく、interfaceであるべき理由がわかってもらえたのではないだろうか。
さらに余談になるが、オブジェクトの定義でも、Object.createを使用することで、定義したオブジェクトを元に新たなインスタンスを作ることも可能だ。これらの理由から、オブジェクトもクラス同様に扱えるため(一部を除いて)、typeではなくinterfaceを用いるということだ。

ESLintの設定

これもまた、賛否がある内容だが以下のような定義の場合、各メンバー末尾に記号をどのようにする設置するのかも議論の対象だ。

interface User {
    name: string;
}
interface User {
    name: string,
}
interface User {
    name: string
}

困ったことに、ここに関するエラーは初期のESLintで出てきた経験は殆どない。よって自ら設定することが多いが、利用しているのは、@stylistic/eslint-pluginだ。他にもルールがあるため、レコメンドを使用するのもありだ。

セミコロンなし

ここまでの記述で気づいた人もいるかも知れないが、基本的に私の書く、JavaScriptとTypeScriptはセミコロンを記述しない。書くのが面倒であることや、セミコロンが存在しない言語もあることから、セミコロンは使用しない。頭が混乱するのはセミコロン必須の言語を触れるときくらいだが、それはJavaScriptに限った話ではない。そもそも、JavaScript自体が、セミコロンの有無どちらでもいいという特殊性があるため、無い方を選んだにすぎない。

そして、セミコロンなしでTypeScriptを記述しているため、interfaceでの定義も自ずと同じ表現になるというわけだ。

interface IUser {
    name: string
    age: number
}

class User implements IUser {
    name = 'bob'
    age = 2024
}

interfaceでセミコロンを設定しないという点については、そもそもコード上で書かないため除外され、カンマを用いない理由として、オブジェクトの定義と見間違うことがよくあったので、何も区切り文字を使用しない結論に至った。横に続けて書く場合は、仕方ないので、カンマとなるが、可能な限り1行で書かず改行している。

interface IUser { name: string, age: number }

まとめ

様々な議論の余地もあり、スタンダードといえる結論が出ていないようにも思えるし、人によっては、何かしらの信念がある場合もない場合もあると思う。ただ、複数人での開発は、効率や認識のしやすさ、分け方などが大切になってくると思う。そのため、これからの人は是非検討してほしいし、これを見た人で、すでに何かしらの面白い結論に至っているのであれば、それを紹介してほしい。

Discussion