メインコンテンツへスキップ

Value Objectの代わりにBranded Typesを使う

·239 文字·2 分
Captain Emo
著者
Captain Emo
ドメイン知識の抽出、テスト作成、React/Nodeを得意としています。

プリミティブ型にドメイン知識を足すにはValue Objectを使えばよいのですが、なるべくclassは書きたくありません。

そこでBranded Typesを使います。

💡 TypeScript公式では “Nominal Typing” と呼ばれています。

https://typescript-jp.gitbook.io/deep-dive/main-1/nominaltyping#nominal-typing

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',
});

まとめ #

ドメイン知識の型表現でご安全に。