🏷️

【TypeScript】新しい Branded Types

に公開

問題

従来の Branded Types にはスプレッドでブランドが流出するという問題があります。

type Brand<T extends symbol> = {
  [K in T]: unknown;
};

declare const dateRangeBrand: unique symbol;

type DateRange = {
  readonly start: Date;
  readonly end: Date;
} & Brand<typeof dateRangeBrand>;

const createDateRange = ({
  start,
  end,
}: {
  start: Date;
  end: Date;
}): DateRange => {
  if (start >= end) {
    throw new Error();
  }
  return { start, end } as DateRange;
};

const validDateRange = createDateRange({
  start: new Date('2025-01-01T10:00:00'),
  end: new Date('2025-01-01T15:00:00'),
});

const invalidDateRange: DateRange = {
  ...validDateRange,
  start: new Date('2025-01-01T20:00:00'),
}; // 型エラーにならない

バリデーション済みのオブジェクトをスプレッドすると、ブランドが保持されます。これにより、バリデーションを経ていない値がバリデーション済みなものとして扱われてしまいます。

解決策

private フィールドを持つクラスを定義します。

declare class Brand<T extends symbol> {
  private readonly __brand: {
    [K in T]: unknown;
  };
}

declare const dateRangeBrand: unique symbol;

type DateRange = {
  readonly start: Date;
  readonly end: Date;
} & Brand<typeof dateRangeBrand>;

const createDateRange = ({
  start,
  end,
}: {
  start: Date;
  end: Date;
}): DateRange => {
  if (start >= end) {
    throw new Error();
  }
  return { start, end } as DateRange;
};

const validDateRange = createDateRange({
  start: new Date('2025-01-01T10:00:00'),
  end: new Date('2025-01-01T15:00:00'),
});

const invalidDateRange: DateRange = {
  ...validDateRange,
  start: new Date('2025-01-01T20:00:00'),
}; // 型エラー

スプレッドすると、型上 private フィールドが失われます。すなわちブランドの流出を防ぐことができます。

まとめ

Branded Types にクラスの private フィールドを使うことで、スプレッドによるブランドの流出を防げます。

mutex Tech Blog

Discussion