Branded Types
課題
typeを使って定義できるのは型エイリアスであり、同じ構造を持つ型があると、コンパイル時に区別できない(もちろん実行時も区別できない)。区別したいものがオブジェクト型であれば、タグを埋め込むなどして区別しやすくできるが、「○○ID」のようにプリミティブ型だと、そういった手法を採用できない。
Branded Typesが優れているのは、ここでプリミティブをそのまま異なる型とみなせるようにしたことです。コンパイラには異なる型とみなさせているだけで、ECMAScriptとしての実行時には余計なプロパティやメソッドへのアクセスがありません。
また、オブジェクトではないということは、JSONのシリアライズにも強いです。class Userの場合はJSON.stringify()の結果は{"v":"abcde12345"}となってしまい、vプロパティの中に値の実体が存在することが露出してしまいます。Branded Typesの場合はプリミティブであるためシリアライズしても"abcde12345"のみが得られます。
定義
code:memo.ts
type Underscore<P extends string> = __${P};
type Brand<K, T extends string> = K & Underscored<T>;
type FilledString = Brand<string, "FilledString">;
type UserId = Brand<FilledString, "UserId">;
// ^? string & Underscored<"FilledString"> & Underscored<"UserId">
解説
素朴なBranded Typesの定義だとtype Brand<K, T> = K & { __brand: T }とすることが多い。そのように定義すると、上述の利用例のようにBrand<Brand<string, "FilledString">, "UserId">といった具合にBrand<K, T>をネストさせたものがコンパイルエラーになってしまう({ __brand: "FilledString" } & { __brand: "UserId" }がnever型になるため)。
そこで__brandタグの名前のほうを利用側で指定できるよう、& { __brand: T }の部分を工夫している。
具体的には、TをT extends stringとしてstring型に限定したうえで、{ __${T}: T }という型を作り出し、タグの名前がなるべく重複しないようにしている。