スプレッド構文によるブランド流出問題を乗り越えて、オブジェクト型に対する Branded Types を使い倒す @TSKaigi 2026

1.5K Views

May 23, 26

スライド概要

profile-image

東京都でソフトウェアエンジニアをやっています。チャーチ=チューリングのテーゼに懐疑的です。

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

スプレッド構文によるブランド流出問題を乗り越えて、 オブジェクト型に対する Branded Types を使い倒す tony 株式会社 mutex TSKaigi 2026 - 2026/05/23

2.

自己紹介 tony 株式会社 mutex / ソフトウェアエンジニア @tony998244353 社では BE を担当 TypeScript 歴約 1 年 C/C++, Java, Python, Rust, Swift 等経験あり 最近の推しは Lean 格闘技観戦が趣味 TSKaigi 2026 - 2026/05/23 1 / 11

3.
[beta]
Branded Types

TypeScript において公称型を実現するためのハック
brand.ts

export type Brand<T extends symbol> = { readonly [K in T]: unknown };
user.ts

import type { Brand } from "./brand";
declare const userIdSymbol: unique symbol;
export type UserId = string & Brand<typeof userIdSymbol>;
export type User = { readonly id: UserId; readonly name: string };
export const createUser = (name: string): User => ({ id: crypto.randomUUID() as UserId, name });
main.ts

import { createUser, type User } from "./user";
const validUser: User = createUser("Alice");
const invalidUser: User = { id: "u_123", name: "Bob" };

TSKaigi 2026 - 2026/05/23

//

型エラー: id が UserId ではない
2 / 11

4.
[beta]
オブジェクト型に対する Branded Types

オブジェクト型を含む一般的な型に対しても、ドメイン制約の表現のために Branded Types は有効
date-range.ts

import type { Brand } from "./brand";
declare const dateRangeSymbol: unique symbol;
export type DateRange = { readonly start: Date; readonly end: Date } & Brand<typeof dateRangeSymbol>;
export const createDateRange = (start: Date, end: Date): DateRange => {
if (start >= end) throw new Error("start must be before end");
return { start, end } as DateRange;
};
main.ts

import { createDateRange } from "./date-range";
const validDateRange = createDateRange(
new Date("2026-05-23T16:40:00"),
new Date("2026-05-23T17:10:00"),
);

TSKaigi 2026 - 2026/05/23

3 / 11

5.
[beta]
従来の Branded Types の罠
スプレッド構文でブランドが流出する
main.ts

/*

確認: DateRange は以下のような構造をしている
{

readonly start: Date;
readonly end: Date;
readonly [dateRangeSymbol]: unknown;
}

ブランドは所詮ただのプロパティであることに注意

*/

const invalidDateRange: DateRange = {
...validDateRange,
start: new Date("2026-05-23T17:10:00"),
end:
new Date("2026-05-23T16:40:00"),
}; //

型エラーにならない 😱

TSKaigi 2026 - 2026/05/23

4 / 11

6.
[beta]
今回のスコープ

スプレッド構文でブランドが流出しないような
Branded Types の新たな実装パターンの提案

想定疑問

Q. class 使ったらよくない?
A. class は直積構造前提で一般性を欠くので好きじゃない (小並感)

そうでなくても最近は class 自体わりと避けられる傾向にある
Q. スプレッド構文に似た機能を持つ Object.assign についてはどうなの?
A. <T extends {}, U>(target: T, source: U) => T & U はそもそも型安全じゃないのでケアしない
(同名プロパティについて後勝ちだが、それぞれの型が異なる場合壊れる)
TSKaigi 2026 - 2026/05/23

5 / 11

7.

鍵: class の private フィールド フィールドは class の外から不可視 スプレッド構文は可視プロパティのみを展開する private class Hoge { x: number = 0; private y: number = 0; } const hoge = new Hoge(); const fuga = { ...hoge }; // fuga: { x: number } ← y は出てこない → ブランドも同様にカプセル化できそう TSKaigi 2026 - 2026/05/23 6 / 11

8.
[beta]
できた!
brand.ts

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

スプレッド時にブランドを落とすための private
private を使うためだけの class
ランタイム実体はいらないので declare
<T extends symbol>

TSKaigi 2026 - 2026/05/23

+ [K in T]: unknown は従来パターンの踏襲
7 / 11

9.
[beta]
使用例

内部が変わったのみで、使い方は変更なし

Brand
main.ts

import { createDateRange, type DateRange } from "./date-range";
const validDateRange = createDateRange(
new Date("2026-05-23T16:40:00"),
new Date("2026-05-23T17:10:00"),
);
const invalidDateRange: DateRange = {
...validDateRange,
start: new Date("2026-05-23T17:10:00"),
end:
new Date("2026-05-23T16:40:00"),
}; //

ちゃんと型エラー ✅

TSKaigi 2026 - 2026/05/23

8 / 11

10.
[beta]
パッケージ境界における注意点
private

版を .d.ts 経由で利用するとブランドが区別されなくなる (microsoft/TypeScript#38953)

brand.d.ts

export declare class Brand<T extends symbol> {
private readonly __brand; //
}

型注釈ごと剥がれる

→ 元々 Brand<X> と Brand<Y> の差は __brand の型のみ
型が落ちれば構造的に同型となり、別ブランドが混ざる

は型が落ちる
実装隠蔽が目的なので、利用側の型情報に晒すべきでない
としてエミッタが意図的に剥がす
private

は型が残る
サブクラスから見える前提なので、継承時の型検査のため
.d.ts でも保持される
protected

→ private を protected に変えればブランドの区別が保たれる
TSKaigi 2026 - 2026/05/23

9 / 11

11.

実運用と移行容易性 弊社プロダクトで 35 ファイル / 51 種類のブランド型を、本発表の declare class Brand + protected で運用中 適用範囲: ドメインモデル全体を統一的に branded 化 エンティティ、ID 型、列挙値、不変条件付きの小さな値オブジェクト、etc. ドメイン上「不正な値が混入し得る型」は基本すべて対象 オブジェクト型にブランドを被せる心理的障壁が消えた スプレッドでブランドが流出する罠を気にしなくて済むため、複合ドメイン型にも遠慮なく適用できる タイトル通り Branded Types を「使い倒せる」状態に すでに unique symbol の Branded Types を運用しているプロジェクトなら Brand の 定義を差し替えるだけ 既存のドメイン型・ファクトリ関数は修正不要で、導入リスクが低い TSKaigi 2026 - 2026/05/23 10 / 11

12.

まとめ Branded Types はオブジェクト型のドメイン制約にも有効 従来パターンにはスプレッド構文でブランドが流出する罠がある declare class + private フィールドで「型上は外から見えない」性質を活かし、流出を防げ る パッケージ境界をまたぐなら protected に置き換えれば安全 弊社プロダクトで実運用中。 Brand を差し替えるだけで導入できる Thank you! TSKaigi 2026 - 2026/05/23 11 / 11