elm-typedというパッケージを作った。詳しくは以下のGithubのREADMEに書いている。
パッケージ自体が根本的に実現したいことはPunie/elm-idとかjoneshf/elm-taggedあたりとほぼほぼ同じ。プリミティブ型をそのまま使わず型エイリアスでもなく、プリミティブ型をラップしたカスタム型を使うことで仕様をもっと型に表現したいというモチベーションから来ている。
実際に使う場合には、以下のようにelm-typedが提供している Typed
型に対して、タグとなる型(MemberIdType
とかAgeType
あたり)とその型が内部的に持つプリミティブ型、そして最後にパーミッション(後述)を指定する。
import Typed exposing (Typed, ReadOnly, ReadWrite) type MemberIdType = MemberIdType type alias MemberId = Typed MemberIdType String ReadOnly type AgeType = AgeType type alias Age = Typed AgeType Int ReadWrite type alias Model = { memberId : MemberId , age : Age }
パーミッションはelm-idとelm-taggedを折衷するための機構で、ある Typed
なデータに対して任意のパーミッションを指定することで、Elmアプリケーションにおけるそのデータの作成/更新を制限することができる*1。
たとえば、上のサンプルでは MemberId
は ReadOnly
に指定されているため、newやmapなど更新系関数の呼び出しはできない。例外的に ReadOnly
であっても encode/decode はできるようになっているが、Elmアプリケーションに値が生まれた時点ではもう書き込み不可である。
newAge : Age newAge = Typed.new 30 -- OK! newMemberId : MemberId newMemberId = Typed.new "1" -- ReadOnly指定なので新規にデータを作成できない(コンパイルエラーになる)
パーミッションの利点として、フロントエンド・アプリケーションにおいて参照のみで使うデータ(サーバーサイドから取得されるIDなど)をReadOnly指定することで、仕様上更新してはいけないデータを更新してしまうなどのバグを未然に防止できる。また、コードレビューやコードリーディングの際にもModelを読むだけでどのデータが更新される仕様なのかが把握しやすい。
実際に個人開発のElmアプリケーションでこのパッケージをテスト導入をしているが、それまでOpaque Typeなモジュールとして設計していた型が置き換えられていてイイ感じ。カスタム型による仕様の表現だけじゃなく、型の定義でデータのライフサイクルまで制御できるのはわりかし便利だ。