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

[typescript]

[zod]

[branded types]

[value object]

プリミティブ型にドメイン知識を足すには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',
});

まとめ

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

[rust] macOSでwindows用にクロスコンパイルしたい
`TypeError: Unknown file extension ".ts"` エラー