TypeScriptアドベンドカレンダー 3日目の記事になります。
TypeScriptの実装パターンとして、Branded Types と Companion Object というものがあります。
まずはこの2つについて簡単に説明します。
Branded Types(ブランド化型)
「ユーザーID」と「記事ID」などのように、どちらも文字列(string)であるものを型レベルで区別して扱えるようにすることにより、型安全に扱えるようにするパターンです。
オブジェクト指向の言語でいうところの「プリミティブ型禁止」の規約をTypeScriptの型システムによって簡単に実現できるようにしたものだと思っています。
// 汎用的に使えるBrand型を作っておく
type Brand<K, T> = K & { _brand: T };
// それぞれの型を作成
type UserId = Brand<string, "UserId">;
type EntryId = Brand<string, "EntryId">;
// 変数作成時に型付け
let userId1 = "U001" as UserId;
let userId2 = "U002" as UserId;
let entryId1 = "E001" as EntryId;
// OK
userId1 = userId2;
// コンパイルエラー
userId1 = "aaa";
userId1 = entryId1;
// stringのまま扱える
console.log(userId1.length);
参考
https://scrapbox.io/mrsekut-p/branded_types
https://typescript-jp.gitbook.io/deep-dive/main-1/nominaltyping
Companion Object(コンパニオン・オブジェクト)
型と値(オブジェクト)を同名で定義できるというTypeScriptの仕組みを使い、型に関係の深い実装を型と同名のオブジェクトに入れ込む実装パターンです。
Javaなどにおけるinterfaceのデフォルト実装に近いものだと思います。
export type Rectangle = {
height: number;
width: number;
};
export const Rectangle = {
from(height: number, width: number): Rectangle {
return {
height,
width,
};
},
};
import { Rectangle } from "./rectangle";
const rec: Rectangle = Rectangle.from(1, 3);
console.log(rec.height);
console.log(rec.width);
参考(上記コード例もリンク先のものです)
組み合わせると使い勝手が良い
Branded Types のコード例を見てもわかるとおり、IDなどに特有の型を作ったとしても、それを生成する際に as などを使って型付けしてやる必要があり、この部分が実装のあちこちに散らばると、そこが結局型安全ではなくなってしまいます。
Brandedな型と同名のオブジェクトを Companion Object として作成し、その中でBrandedな値を生成するメソッドを定義し、Brandedな値を作るときは必ずそこを経由するようにしておくと、あっちこっちで as をつける必要もなく、可読性も良くなります。
export type Brand<K, T> = K & { _brand: T };
export type UserId = Brand<string, "UserId">;
export const UserId = {
of(id: string): UserId {
return id as UserId;
}
}
const userId1 = UserId.of("U001");
以上、小ネタではありますが、個人的には使う頻度の高い実装パターンでした。