TECH BOX

Technology blog from Web Engineer

この記事は最終更新日から4年以上経過しているため正確ではないかもしれません

型付けを賢く使う

TypeScriptは型が付けられるのが魅力の一つです。
型があると制約を課すことができるので、予期せぬバグを未然に防ぐことができます。
(例: 数値を扱うはずなのに誤って文字列を代入してしまうとエラーになる)

TypeScriptの型にはいくつかあって、型推論(初期代入した値を自動で型にする)、クラス、InterfaceType…。

今回はInterfaceTypeを駆使すると幸せになる方法を紹介します。

Interface

Interfaceはオブジェクトの型を定義するときに使います。
とはいえ、どういうときに使うのが便利なのか分かりづらいと思います。

一番わかり易いのがメソッドや関数の引数がオブジェクトの場合です。

Interfaceを使わない場合

const obj: { id: number; name: string; birthday: string; nickname?: string } = {
  id: 111,
  name: 'nanashi',
  birthday: '2019-xx-xx'
}

// なんらかの条件がある場合にキーを追加する場合
if (bar) {
  obj.nickname = 'dare?'
}

const res = data(obj)

// 何かの実行関数
function data(obj: {
  id: number
  name: string
  birthday: string
  nickname?: string
}): { display: string; age: number } {
  // 何らかの処理
  return {
    display: obj.nickname || obj.name,
    age: 10
  }
}

素直な実装であればInterfaceを使わなくても不要ですが、上記のように関数に渡す引数が、何らかの条件によって変わるような場合にはobj変数にも型を指定する必要があります。
※型を指定しない場合はobj.nickname部分はエラーになる

この時変数obj引数objは必ず一致している必要があるので、同じことを何度も書くのは面倒になります。
引数が増えれば増えるほどさらに面倒が増えます。

そこでInterfaceを使います。

Interfaceを使った場合

interface DataObj {
  id: number
  name: string
  birthday: string
  nickname?: string
}

const obj: DataObj = {
  id: 111,
  name: 'nanashi',
  birthday: '2019-xx-xx'
}

// なんらかの条件がある場合にキーを追加する場合
if (bar) {
  obj.nickname = 'dare?'
}

const res = data(obj)

function data(obj: DataObj): { display: string; age: number } {
  // 何らかの処理
  return {
    display: obj.nickname || obj.name,
    age: 10
  }
}

最初にInterfaceを追加したことでちょっとスッキリしました。
ただ、これだけだとメリットが無いように感じます。

それもそのはず。
このロジックはここで完結しているからです。

実務においては複数のファイルから複数の関数を呼び出したり、その関数内からさらに処理のために他の関数を呼んだりするということが考えられます。
そのときに、Interfaceで型を統括しなかった時はどうなるでしょうか?

一つ一つ処理を追って修正をしなければいけなくなります。
その修正箇所を探すのは数が多ければ多いほど探しづらくなります。
そうなってしまうとメンテナンスしづらくなり、バグの温床になります。

しかし、Interfaceを用意することで該当する情報すべての事象に対して影響を及ぼせます。
つまり、そのInterfaceに派生する処理も修正する対象として見つけやすくなります。
場合によってはビルドエラーになって早期発見もできますし、IDEなどインテリセンスが使えるのであれば逆引きもできます。

Type

もう一つTypeという型の付け方があります。
type Foo = stringというように宣言します。
Typeは上記のように単一の型指定もできますがオブジェクトも指定できます。

Typeの書き方

type Foo = string

type Obj = {
  id: number
  name: string
}

Typeを使う

型をあらかじめ宣言しておくという意味ではInterfaceと似ています。
違いはいろいろありますが、大きな違いとしてInterfaceは同名のInterfaceを宣言するとマージを行うのに対し、Typeは同名をつけることができない点です。
今回はその違いの話ではないので軽く触れるだけにしておきます。

さて、Typeを使うと何が嬉しいのか…。
仮にAPIからデータを受け取るために下記のようにInterfaceを定義したとします。

interface ApiMember {
  id: number
}

interface ApiMemberPage {
  id: number

  // ApiMember.idを指している
  mid: number
}

この時、ApiMemberのidとApiMemberPageのmidは同じことを指していた場合、パッと見て同じかはわかりません。
では、Typeを使ってみます。

type MemberID = number

interface ApiMember {
  id: MemberID
}

interface ApiMemberPage {
  id: number
  mid: MemberID
}

こうするとどうでしょう?
単にnumberと書くよりは何を指しているのかわかりやすくなりました。

何が嬉しいかというと、最初の書き方だとそれぞれが何を指しているのか関連性があるのかは仕様書を見ないとわからないですが、typeでグルーピングすることで関連性をソースコードレベルで認識できる点で違います。

また、こういう事もできます。

class PageData {
  private _pageId: number
  public constructor(id: number) {
    this._pageId = id
  }

  public name(id: MemberID): string {
    return `メンバー名(ID:${id})`
  }
}

const page: ApiMemberPage = { id: 10, mid: 1 }
const pages = new PageData(page.id)
const name = pages.name(page.mid)

もちろんPageData.nameの引数をmidmemberIdなどの変数にしてもいいですが、渡す値がmidの例のように必ずしも分かりやすいものとは限りません。
しかし、typeで定義しておけばこの引数にはMemberIDで宣言されている値を渡せるということが認識しやすくなります。

コードを書く上で解読しやすいというのはメンテナンスがしやすいということになります。


InterfaceとTypeにはいろいろ違いがあるもののどちらも有用です。
ただ、使う時はどういう時にどう使うかをあらかじめ決めておいたほうがいいです。
ルールなく混在させると、結果わかりにくくなることもあるので。

InterFaceやTypeを使いこなしていくとよりよいコードになっていきますので、もし知らなかった!という人はこれから取り入れていってください。