Value Objectの代わりにBranded Typesを使う
プリミティブ型にドメイン知識を足すにはValue Objectを使えばよいのですが、なるべくclassは書きたくありません。
そこでBranded Typesを使います。
Branded Typesとは
次のようにプリミティブを拡張するユーティリティタイプを作り、新しい型を定義します。
brandは焼き印のことです。
// Tがプリミティブの型、Bが新しく作る型名type Branded<T, B extends string> = T & { '__brand': B;};
type ProjectId = Branded<string, 'ProjectId'>;
上記の例ではキーを文字列にしましたが、文字列の代わりにunique symbolを使うことで、 Bに同じ文字列が入っても別の型として定義できます。
多人数で開発しているときはこちらのほうがおすすめです。
💡 unique symbol https://www.typescriptlang.org/docs/handbook/symbols.html#unique-symbol
declare const __brand: unique symbol;type Branded<T, B extends string> = T & { [__brand]: B;};
便利な使いかた
IDに任意の文字列が入るのを防ぐ
/** ドメインタイプの定義 **/type ProjectId = Branded<string, 'ProjectId'>;type Project = { readonly id: ProjectId; name: string;}
/** データ生成 **/// 型アサーションするconst project1: Project = { id: 'p-001' as ProjectId, name: 'hoge'};
// idにstringをそのまま入れるとエラーになるconst project2: Project = { id: 'p-002', // error TS2322: Type 'string' is not assignable to type 'ProjectId'. name: 'hoge'};
貨幣計算で別の貨幣が混ざるのを防ぐ
type JPY = Branded<number, 'CurrencyJP'>;type USD = Branded<number, 'CurrencyUS'>;type Currency = JPY | USD
function add<T extends Currency>(a: T, b: T) { return ( a as number + b as number ) as T;}
const usd100 = 100 as USD;const jpy100 = 100 as JPY;const total = add<JPY>(usd100, jpy100); // error TS2345: Argument of type 'USD' is not assignable to parameter of type 'JPY'.
zodを使ったブランド化とバリデーション
上記のように自分で Branded
を定義して使う他に、バリデーションライブラリの zod でも同じことができます。
zodを使うメリットは、型定義とバリデーションを一度に行えることです。 また、型アサーションも必要ないので、JSONをそのままパースすることができます。
import { z } from "zod";
// バリデーションスキーマ定義const projectSchema = z.object({ id: z.string().max(5).readonly().brand('ProjectId'), name: z.string(),}).required();
// 型定義type Project = z.infer<typeof projectSchema>;
// データ生成const project = projectSchema.parse({ id: 'p-001', name: 'hoge',});
まとめ
ドメイン知識の型表現でご安全に。