🏷️
【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 フィールドを使うことで、スプレッドによるブランドの流出を防げます。
Discussion