不透明型
型の内部構造を隠蔽し、モジュール外部からの直接操作を防ぐカプセル化テクニック
不透明型とは
不透明型 (Opaque Type) は、型の内部表現 (例: string) を外部に公開せず、専用のコンストラクタ関数を通じてのみ値を生成できるようにするカプセル化テクニックである。「この値はバリデーション済みである」ことを型システムで保証し、不正な値の混入をコンパイル時に防ぐ。
問題: プリミティブ型の取り違え
TypeScript では string 型の値はすべて互換性がある。メールアドレスもユーザー ID も URL も、型システム上は同じ string だ。
function sendEmail(to: string, subject: string, body: string) { ... }
// 引数の順番を間違えても、コンパイルエラーにならない
sendEmail(subject, to, body); // バグだが型チェックを通過する
不透明型はこの問題を解決する。Email 型と Subject 型を別の型として定義すれば、取り違えはコンパイルエラーになる。
TypeScript での実装
ブランド型 (基本形)
type Email = string & { readonly __brand: 'Email' };
type UserId = string & { readonly __brand: 'UserId' };
// Email と UserId は互換性がない
function sendEmail(to: Email, subject: string) { ... }
const userId: UserId = 'user-123' as UserId;
sendEmail(userId, 'Hello'); // コンパイルエラー ✅
ただし、ブランド型は as でキャストすれば外部からも生成できてしまう。
不透明型 (強化版)
// opaque-types.ts
declare const __brand: unique symbol;
type Email = string & { readonly [__brand]: 'Email' };
// モジュール内でのみ Email を生成できる
export function createEmail(input: string): Email {
if (!/^[^@]+@[^@]+\.[^@]+$/.test(input)) {
throw new Error('Invalid email format');
}
return input as Email;
}
// 外部ではブランドのキー名を書けないため、値を組み立てて代入する経路は塞がれる
// const email: Email = 'test@example.com'; // 代入不可
// 一方、型アサーションは基底型からの絞り込みとして成立するので止まらない
// const email = 'test@example.com' as Email; // これはコンパイルエラーにならない
unique symbol が塞ぐのは「ブランドのキー名を外部で書くこと」である。同じ形の型を別のモジュールで宣言して代入する迂回路はこれで消えるし、Email と UserId のようにブランドの異なる型を直接キャストするのもエラーになる。一方で as Email そのものは基底型 string からの絞り込みとして成立するため、外部からでも通ってしまう (TypeScript 6.0 で実測)。つまり生成経路を createEmail に固定したいなら、型定義だけでは足りない。型アサーションを lint やコードレビューで禁じる運用と組み合わせて初めて、バリデーション済みという前提が守られる。
ブランド型と不透明型の違い
ブランド型と不透明型の主な違いを以下に整理する。
| 観点 | ブランド型 | 不透明型 |
|---|---|---|
| 外部からのキャスト | as で可能 | as で可能 (基底型からの絞り込み) |
| 外部での型の偽造 | 同じ形の型を宣言して代入できる | キー名が書けないため塞がれる |
| バリデーション保証 | 弱い (キャストで迂回可能) | 型だけでは完結せず、アサーション禁止の運用が前提 |
| 実装の複雑さ | シンプル | やや複雑 |
| 適するケース | チーム内の軽い型安全性 | ライブラリの公開 API、セキュリティ要件 |
実務での活用パターン
ID の取り違え防止
type UserId = string & { readonly [__brand]: 'UserId' };
type OrderId = string & { readonly [__brand]: 'OrderId' };
// UserId と OrderId を間違えるとコンパイルエラー
async function getOrder(orderId: OrderId): Promise<Order> { ... }
const userId = createUserId('user-123');
getOrder(userId); // コンパイルエラー ✅
DynamoDB のシングルテーブル設計では、PK に USER#123、ORDER#456 のようなプレフィックス付き ID を使う。不透明型で ID の種類を区別すれば、誤ったパーティションキーでクエリするバグを防げる。
セキュリティ上の区別
type PlainPassword = string & { readonly [__brand]: 'PlainPassword' };
type HashedPassword = string & { readonly [__brand]: 'HashedPassword' };
// ハッシュ化済みパスワードと平文パスワードを型で区別
function hashPassword(plain: PlainPassword): HashedPassword { ... }
function verifyPassword(plain: PlainPassword, hashed: HashedPassword): boolean { ... }
// 平文パスワードをそのまま DB に保存しようとするとコンパイルエラー
function saveUser(name: string, password: HashedPassword) { ... }
バリデーション済み URL
type ValidUrl = string & { readonly [__brand]: 'ValidUrl' };
export function parseUrl(input: string): ValidUrl {
new URL(input); // 不正な URL なら例外
return input as ValidUrl;
}
他言語での不透明型
他言語での不透明型を以下にまとめる。
| 言語 | 実現方法 |
|---|---|
| Haskell | newtype で既存型をラップ (ランタイムコストゼロ) |
| Rust | タプル構造体 struct Email(String) |
| Flow | opaque type キーワード (言語レベルでサポート) |
| Scala | opaque type (Scala 3) |
| TypeScript | ブランド型 + unique symbol (ワークアラウンド) |
TypeScript は言語レベルで不透明型をサポートしていないため、ブランド型のテクニックで代用する。Flow の opaque type は、定義したファイルの外では内部の型を隠すという言語仕様であり、キャストの抜け道を運用で塞ぐ必要がない点が TypeScript との差になる。
よくある落とし穴
- 過剰な適用: すべての
stringを不透明型にすると、コードが冗長になる。セキュリティ上重要な値 (パスワード、トークン) や、取り違えが深刻なバグにつながる値 (各種 ID) に限定して適用する - ランタイムの型消去: TypeScript の型情報はコンパイル時に消去される。不透明型はコンパイル時の安全性のみを提供し、ランタイムでの型チェックは別途必要
- シリアライズ/デシリアライズ: JSON からデシリアライズした値は
string型になる。API レスポンスの受け取り時にコンストラクタ関数を通す処理を忘れないこと - 二段キャストの抜け道:
value as unknown as Emailはブランドの種類を問わず通る。型で塞ぎきれる前提を置かず、値の生成箇所をレビューの観点に入れる
「Programming TypeScript」(Boris Cherny 著) や「Effective TypeScript」(Dan Vanderkam 著) で、ブランド型と型安全性のテクニックが解説されている。
基礎から学ぶなら関連書籍が手がかりになる。
この記事は役に立ちましたか?
関連用語
ブランド型
TypeScript で構造的に同じ型を名目的に区別し、型の取り違えを防ぐテクニック
TypeScript
JavaScript に型を書く手段を足した言語。構造的型付け、型が実行時に残らない仕組み、消せない構文と型定義ファイルの扱いを解説
ファントム型
型パラメーターが値レベルでは使われず、コンパイル時の状態追跡にのみ使用される型テクニック
型安全性
型システムが禁じている操作をプログラムが実行しないことを処理系が保証する性質。静的/動的と強い/弱いの 2 軸、健全性、型検査が破れる経路を解説
型アサーション
TypeScript でコンパイラーに型情報を明示的に伝える構文で、誤用するとランタイムエラーの原因になる
Option / Result 型
null や例外の代わりに、値の有無やエラーを型で表現する関数型プログラミングのパターン
関連する記事
技術書ランキングの決定版はどれか - IT エンジニア本大賞の歴代大賞と言及数データで読む
技術書ランキングを探している人向けに、検証可能な 2 つのデータ (IT エンジニア本大賞の歴代大賞 2014-2026 年と、当サイトの Qiita / Zenn 言及数ランキング) を整理。歴代受賞作の一覧表と、ランキングを選書に活かす方法を解説します。
セキュリティ本ガイド - Web 開発者が読むべき技術書の選び方
Web セキュリティの基礎から実践まで学べる技術書の選び方マトリクスと、読了後にやるべき 3 つのアクションを紹介します。
技術書がエンジニアのキャリアを変える - 読書習慣と年収の関係
技術書の読書習慣がエンジニアのキャリアアップに影響する 3 つの経路と、ジュニア / ミドル / シニア各段階に応じた読書戦略を具体的に解説します。