mcpField
B
branded-types

branded-types

github.com/marius-townhouse開発645 回インストール39 週間前に更新
リポジトリ

branded-types について

TypeScript の型設計スキルで、ブランド型を使い、基底型が同じでも意味の異なる値を区別します。型ガードと検証によって値の混同を防ぎ、実行時のオーバーヘッドもありません。

主な機能

  1. 01意味の異なるプリミティブの区別
  2. 02型ガードによる型の絞り込み
  3. 03単位や識別子へのブランド付け
  4. 04検証済み・サニタイズ済み文字列の追跡
  5. 05実行時オーバーヘッドの回避

活用シーン

  1. 01絶対パスと相対パスの区別
  2. 02単位やベクトル次元の混同防止
  3. 03ユーザー ID と投稿 ID の型安全な分離

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/marius-townhouse/effective-typescript-skills --skill branded-types

インストール後、Claude Code で/branded-typesを入力して読み込まれたか確認します。

詳細

名目的型付けにブランドを検討する

概要

ファントム型を追加して、意味的に異なる値を区別します。

TypeScript は構造的型付けを使用しますが、名目的型付けが必要になることがあります。つまり、形状が異なるからではなく、そう定義することで値を区別します。ブランドを使えば、実行時のオーバーヘッドなしでこれを実現できます。

このスキルを使う場面

  • パスを区別する場合(絶対パスと相対パス)
  • 数値に単位を付ける場合(メートル、秒)
  • 2D ベクトルと 3D ベクトルの取り違えを防ぐ場合
  • 検証済み/サニタイズ済みの文字列を示す場合
  • 型安全な識別子を作成する場合

絶対的なルール

ALWAYS use brands when primitive types have different semantic meanings.

覚えておきましょう:

  • ブランドはファントム型です(型システム内にのみ存在します)
  • 実行時のオーバーヘッドはありません
  • 明示的な変換/検証を強制します
  • 無効な状態を表現できないようにします

検出:「プリミティブ型の取り違え」の問題

異なるプリミティブ型を混同する可能性がある場合:

// ❌ Any string can be passed
function readFile(path: string) { ... }

readFile('foo.txt');        // Relative path - might fail
readFile('/home/foo.txt');  // Absolute path - works
// TypeScript can't tell the difference!

ブランドのパターン

基本的なブランド構造
type AbsolutePath = string & { _brand: 'abs' };

function isAbsolutePath(path: string): path is AbsolutePath {
  return path.startsWith('/');
}

function listAbsolutePath(path: AbsolutePath) {
  // Can only be called with validated paths
}
ブランド型の使用
function f(path: string) {
  // Must check before using
  if (isAbsolutePath(path)) {
    listAbsolutePath(path);  // OK: path is now AbsolutePath
  }
  
  listAbsolutePath(path);
  //               ~~~~ Error: string not assignable to AbsolutePath
}

ブランドが機能する理由

string であり、かつ _brand プロパティを持つ値を実際に作ることはできません。

type AbsolutePath = string & { _brand: 'abs' };

// This intersection is "impossible" at runtime
// But TypeScript still uses it for type checking

AbsolutePath を得る方法は、次のいずれかです。

  1. それを返す関数から受け取る
  2. 型ガードを使って検証し、型を絞り込む
  3. 型アサーションを使う(最終手段)

よくあるブランドのパターン

測定単位
type Meters = number & { _brand: 'meters' };
type Seconds = number & { _brand: 'seconds' };
type MetersPerSecond = number & { _brand: 'm/s' };

const meters = (m: number) => m as Meters;
const seconds = (s: number) => s as Seconds;

function calculateSpeed(distance: Meters, time: Seconds): MetersPerSecond {
  return (distance / time) as MetersPerSecond;
}

const d = meters(100);
const t = seconds(10);
const speed = calculateSpeed(d, t);  // OK

calculateSpeed(100, 10);  // Error: number not assignable to Meters
calculateSpeed(t, d);     // Error: can't swap distance and time!

注意点: 算術演算を行うとブランドは失われます。

const doubled = d * 2;
//    ^? const doubled: number  (brand lost)
検証済み文字列
type SanitizedHTML = string & { _brand: 'sanitized' };
type UserId = string & { _brand: 'userId' };
type Email = string & { _brand: 'email' };

function sanitize(html: string): SanitizedHTML {
  // Actually sanitize the HTML
  return html.replace(/<script>/g, '') as SanitizedHTML;
}

function setInnerHTML(el: Element, html: SanitizedHTML) {
  el.innerHTML = html;  // Safe: we know it's sanitized
}

// Can't pass unsanitized strings
setInnerHTML(el, '<script>alert("xss")</script>');
//               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Error!

// Must sanitize first
setInnerHTML(el, sanitize(userInput));  // OK
型安全な ID
type UserId = string & { _brand: 'userId' };
type PostId = string & { _brand: 'postId' };

function getUser(id: UserId): User { ... }
function getPost(id: PostId): Post { ... }

declare const userId: UserId;
declare const postId: PostId;

getUser(userId);  // OK
getUser(postId);  // Error: PostId not assignable to UserId
ソート済み配列
type SortedList<T> = T[] & { _brand: 'sorted' };

function isSorted<T>(xs: T[]): xs is SortedList<T> {
  for (let i = 0; i < xs.length - 1; i++) {
    if (xs[i] > xs[i + 1]) return false;
  }
  return true;
}

function binarySearch<T>(xs: SortedList<T>, x: T): boolean {
  // Can assume xs is sorted
  let low = 0, high = xs.length - 1;
  while (high >= low) {
    const mid = low + Math.floor((high - low) / 2);
    const v = xs[mid];
    if (v === x) return true;
    [low, high] = x > v ? [mid + 1, high] : [low, mid - 1];
  }
  return false;
}

const nums = [1, 3, 5, 7, 9];
if (isSorted(nums)) {
  binarySearch(nums, 5);  // OK: nums is SortedList<number>
}

代替のブランド付け手法

Unique Symbol の使用(より強力)
declare const brand: unique symbol;

type Meters = number & { [brand]: 'meters' };

// Can't be faked because brand isn't exported
クラスでのプライベートフィールドの使用
class ValidatedEmail {
  private readonly _brand!: 'email';
  constructor(public readonly value: string) {
    if (!value.includes('@')) throw new Error('Invalid email');
  }
}

ベクトルの取り違えを防ぐ

interface Vector2D {
  x: number;
  y: number;
  z?: never;  // Explicitly prevent z
}

function norm(v: Vector2D) {
  return Math.sqrt(v.x ** 2 + v.y ** 2);
}

const v3d = { x: 3, y: 4, z: 5 };
norm(v3d);  // Error: z is incompatible with never

または、ブランドを使います。

type Vector2D = { x: number; y: number } & { _brand: '2d' };
type Vector3D = { x: number; y: number; z: number } & { _brand: '3d' };

プレッシャーへの対処プロトコル

1. 「型エイリアスを使えばよい」

プレッシャー: 「型エイリアスのほうが簡単です。type UserId = string」

回答: 型エイリアスでは、異なる文字列型の取り違えを防げません。

対応: 意味的な区別が重要な場合はブランドを使います。

2. 「実行時のオーバーヘッドがあるだけだ」

プレッシャー: 「プリミティブ型にプロパティを追加するとメモリを消費します」

回答: ブランドはファントム型なので、実行時には存在しません。

対応: 実行時のコストはないため、ブランドを積極的に使いましょう。

危険信号 — 立ち止まって再検討する

  • 取り違える可能性のある文字列型/数値型が複数ある
  • 「任意の文字列」を受け取るものの、特定の形式を想定している関数がある
  • 検証は行われているが、型システムで追跡されていない
  • 型が似た引数を入れ替えたことによるバグがある

よくある正当化(いずれも誤り)

言い訳実際のところ
「注意して使えばよい」特に大規模なコードベースでは、間違いが起こります
「型エイリアスで十分」エイリアスでは異なる型間の代入を防げません
「手続きが多すぎる」発見が難しいバグを防げます

クイックリファレンス

// Basic brand pattern
type Brand<T, B extends string> = T & { _brand: B };

type UserId = Brand<string, 'userId'>;
type Meters = Brand<number, 'meters'>;

// Type guard pattern
function isX(val: T): val is BrandedT { ... }

// Factory pattern
const meters = (n: number) => n as Meters;

要点

プリミティブ型に意味を持たせるために、ブランドを使いましょう。

ブランドは実行時のオーバーヘッドを追加せず、基になる型が同じというだけで値を取り違えることを防ぎます。ID、パス、単位、検証済み文字列など、意味的な区別が重要なプリミティブ型に使いましょう。

参考文献

Dan Vanderkam 著『Effective TypeScript』項目 64「Consider Brands for Nominal Typing」に基づいています。

コメント