こちらの記事は2021年度の「株式会社カケハシ x TypeScript」アドベントカレンダーの7日目の記事になります。
公称型と構造的部分型
TypeScriptは構造的部分型(Structural Subtyping)という型システムを採用していますが、JavaやC++のような公称型(Nominal Typing)相当のことを実現できます。
構造的部分型と公称型の概念の説明は以下の記事が参考になりますので、本稿では詳細な説明は割愛します。
公称型の利点
公称型を利用する利点は、誤ったドメインロジックをコンパイル時に検査できることにあります。
以下は、fp-tsから派生したライブラリであるnewtype-ts (簡易に公称型を扱うことができるライブラリ)のドキュメントの引用です。
type USD = number
type EUR = number
const myamount: USD = 1
declare function change(usd: USD): EUR
declare function saveAmount(eur: EUR): void
saveAmount(change(myamount)) // ok
saveAmount(myamount) // opss... this is also ok because both EUR and USD are type alias of number!
saveAmountメソッドはEUR型を引数で受けるドメインを表しているのに、USD型でも受けることが可能になっています。もしUSDとEURが型システム上で別の型とみなすことができれば、ドメインの構造に沿わない脆弱性な実装がされていてもコンパイル時に早期発見できる可能性が高まります。
以下もnewtype-tsのドキュメントの引用です。Newtypeをextendsすることで公称型として扱うことができます。
import { Newtype, iso } from 'newtype-ts'
interface EUR extends Newtype<{ readonly EUR: unique symbol }, number> {}
// isoEUR: Iso<EUR, number>
const isoEUR = iso<EUR>()
// myamount: EUR
const myamount = isoEUR.wrap(0.85)
// n: number = 0.85
const n = isoEUR.unwrap(myamount)
declare function saveAmount(eur: EUR): void
saveAmount(0.85) // static error: Argument of type '0.85' is not assignable to parameter of type 'EUR'
saveAmount(myamount) // ok
このようにsaveAmountメソッドはEURでしか受け取れない制約をもたらし、コンパイルエラーとすることができます。
ちなみに、公称型をTypeScriptで《より型安全》に実現する方法で紹介されているアプローチ方法を使えば、newtype-tsを利用せずとも公称型を表現することができます。
newtype-tsの利点はIsoとPrismの型クラスを扱うことができることにあります。
IsoとPrism
上記はIsoの型クラスの定義を抜粋したコードです。
Iso<EUR, number>の場合は、EURからnumberを、numberからEURを導出することができます。このようにSとAは1:1交換ができます。ドメインロジックでEUR型で表現し、(HttpやDB等)アダプタではnumberに変換する時などに便利な型クラスです。
上記はPrismの型クラスの定義を抜粋したコードです。
Prism<number, EUR>の場合は、EURからはnumberを導出が可能ですが、numberからEURを導出できる保証はありません。
Optionは値がある場合にSome、ない場合にNoneになるデータ型です。導出できない場合はNoneになります。
Prismは例えば、ユーザー入力をnumberで受けて、EURの事前条件を満たす場合だけEURに変換ができるという制約を表現する時に便利な型クラスです。
以下は、IDと名前と年齢をプロパティに持つアカウントの定義です。prismメソッド(ファクトリ)に渡している関数が事前条件にあたります。Prismを使うことで型の制約を表現することができました。
以下のようにprismインスタンス活用してアカウントを生成することで、事前条件チェックが施されます。
なお、Optionは値の存在有無しか取り扱わないのでエラー情報は引き継ぐことができません。エラー情報を引き継ぎたい場合は、Eitherへと変換します。Eitherについては以下を参考にしてください。
最後に参考のために、返り値がEitherになるようにしたサンプルコードを載せておきます。