---
title: スプレッド構文によるブランド流出問題を乗り越えて、オブジェクト型に対する Branded Types を使い倒す @TSKaigi 2026
tags: 
author: [Yuji Tone](https://docswell.com/user/tony998244353)
site: [Docswell](https://www.docswell.com/)
thumbnail: https://bcdn.docswell.com/page/LELM8VYV7R.jpg?width=480
description: スプレッド構文によるブランド流出問題を乗り越えて、オブジェクト型に対する Branded Types を使い倒す @TSKaigi 2026 by Yuji Tone
published: May 23, 26
canonical: https://docswell.com/s/tony998244353/KMQVLG-2026-05-23-123609
---
# Page. 1

![Page Image](https://bcdn.docswell.com/page/LELM8VYV7R.jpg)

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


# Page. 2

![Page Image](https://bcdn.docswell.com/page/4JMY6GNNJW.jpg)

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


# Page. 3

![Page Image](https://bcdn.docswell.com/page/PJR9PZD479.jpg)

Branded Types
TypeScript において公称型を実現するためのハック
brand.ts
export type Brand&lt;T extends symbol&gt; = { readonly [K in T]: unknown };
user.ts
import type { Brand } from &quot;./brand&quot;;
declare const userIdSymbol: unique symbol;
export type UserId = string &amp; Brand&lt;typeof userIdSymbol&gt;;
export type User = { readonly id: UserId; readonly name: string };
export const createUser = (name: string): User =&gt; ({ id: crypto.randomUUID() as UserId, name });
main.ts
import { createUser, type User } from &quot;./user&quot;;
const validUser: User = createUser(&quot;Alice&quot;);
const invalidUser: User = { id: &quot;u_123&quot;, name: &quot;Bob&quot; };
TSKaigi 2026 - 2026/05/23
//
型エラー: id が UserId ではない
2 / 11


# Page. 4

![Page Image](https://bcdn.docswell.com/page/PEXQ391ZJX.jpg)

オブジェクト型に対する Branded Types
オブジェクト型を含む一般的な型に対しても、ドメイン制約の表現のために Branded Types は有効
date-range.ts
import type { Brand } from &quot;./brand&quot;;
declare const dateRangeSymbol: unique symbol;
export type DateRange = { readonly start: Date; readonly end: Date } &amp; Brand&lt;typeof dateRangeSymbol&gt;;
export const createDateRange = (start: Date, end: Date): DateRange =&gt; {
if (start &gt;= end) throw new Error(&quot;start must be before end&quot;);
return { start, end } as DateRange;
};
main.ts
import { createDateRange } from &quot;./date-range&quot;;
const validDateRange = createDateRange(
new Date(&quot;2026-05-23T16:40:00&quot;),
new Date(&quot;2026-05-23T17:10:00&quot;),
);
TSKaigi 2026 - 2026/05/23
3 / 11


# Page. 5

![Page Image](https://bcdn.docswell.com/page/3EK9YP2VED.jpg)

従来の Branded Types の罠
スプレッド構文でブランドが流出する
main.ts
/*
確認: DateRange は以下のような構造をしている
{
readonly start: Date;
readonly end: Date;
readonly [dateRangeSymbol]: unknown;
}
ブランドは所詮ただのプロパティであることに注意
*/
const invalidDateRange: DateRange = {
...validDateRange,
start: new Date(&quot;2026-05-23T17:10:00&quot;),
end:
new Date(&quot;2026-05-23T16:40:00&quot;),
}; //
型エラーにならない 😱
TSKaigi 2026 - 2026/05/23
4 / 11


# Page. 6

![Page Image](https://bcdn.docswell.com/page/L73W9XYQ75.jpg)

今回のスコープ
スプレッド構文でブランドが流出しないような
Branded Types の新たな実装パターンの提案
想定疑問
Q. class 使ったらよくない？
A. class は直積構造前提で一般性を欠くので好きじゃない (小並感)
そうでなくても最近は class 自体わりと避けられる傾向にある
Q. スプレッド構文に似た機能を持つ Object.assign についてはどうなの？
A. &lt;T extends {}, U&gt;(target: T, source: U) =&gt; T &amp; U はそもそも型安全じゃないのでケアしない
(同名プロパティについて後勝ちだが、それぞれの型が異なる場合壊れる)
TSKaigi 2026 - 2026/05/23
5 / 11


# Page. 7

![Page Image](https://bcdn.docswell.com/page/87DKG25WJG.jpg)

鍵: 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


# Page. 8

![Page Image](https://bcdn.docswell.com/page/VJPK31GXE8.jpg)

できた！
brand.ts
export declare class Brand&lt;T extends symbol&gt; {
private readonly __brand: {
readonly [K in T]: unknown;
};
}
スプレッド時にブランドを落とすための private
private を使うためだけの class
ランタイム実体はいらないので declare
&lt;T extends symbol&gt;
TSKaigi 2026 - 2026/05/23
+ [K in T]: unknown は従来パターンの踏襲
7 / 11


# Page. 9

![Page Image](https://bcdn.docswell.com/page/2EVV4Y63EQ.jpg)

使用例
内部が変わったのみで、使い方は変更なし
Brand
main.ts
import { createDateRange, type DateRange } from &quot;./date-range&quot;;
const validDateRange = createDateRange(
new Date(&quot;2026-05-23T16:40:00&quot;),
new Date(&quot;2026-05-23T17:10:00&quot;),
);
const invalidDateRange: DateRange = {
...validDateRange,
start: new Date(&quot;2026-05-23T17:10:00&quot;),
end:
new Date(&quot;2026-05-23T16:40:00&quot;),
}; //
ちゃんと型エラー ✅
TSKaigi 2026 - 2026/05/23
8 / 11


# Page. 10

![Page Image](https://bcdn.docswell.com/page/57GL19NYEL.jpg)

パッケージ境界における注意点
private
版を .d.ts 経由で利用するとブランドが区別されなくなる (microsoft/TypeScript#38953)
brand.d.ts
export declare class Brand&lt;T extends symbol&gt; {
private readonly __brand; //
}
型注釈ごと剥がれる
→ 元々 Brand&lt;X&gt; と Brand&lt;Y&gt; の差は __brand の型のみ
型が落ちれば構造的に同型となり、別ブランドが混ざる
は型が落ちる
実装隠蔽が目的なので、利用側の型情報に晒すべきでない
としてエミッタが意図的に剥がす
private
は型が残る
サブクラスから見える前提なので、継承時の型検査のため
.d.ts でも保持される
protected
→ private を protected に変えればブランドの区別が保たれる
TSKaigi 2026 - 2026/05/23
9 / 11


# Page. 11

![Page Image](https://bcdn.docswell.com/page/4EQYDWQ6JP.jpg)

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


# Page. 12

![Page Image](https://bcdn.docswell.com/page/KJ4WZKNM71.jpg)

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


