TypeScriptは型が付けられるのが魅力の一つです。
型があると制約を課すことができるので、予期せぬバグを未然に防ぐことができます。
(例: 数値を扱うはずなのに誤って文字列を代入してしまうとエラーになる)
TypeScriptの型にはいくつかあって、型推論(初期代入した値を自動で型にする)、クラス、Interface、Type…。
今回はInterfaceやTypeを駆使すると幸せになる方法を紹介します。
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
の引数をmid
やmemberId
などの変数にしてもいいですが、渡す値がmid
の例のように必ずしも分かりやすいものとは限りません。
しかし、typeで定義しておけばこの引数にはMemberID
で宣言されている値を渡せるということが認識しやすくなります。
コードを書く上で解読しやすいというのはメンテナンスがしやすいということになります。
InterfaceとTypeにはいろいろ違いがあるもののどちらも有用です。
ただ、使う時はどういう時にどう使うかをあらかじめ決めておいたほうがいいです。
ルールなく混在させると、結果わかりにくくなることもあるので。
InterFaceやTypeを使いこなしていくとよりよいコードになっていきますので、もし知らなかった!という人はこれから取り入れていってください。