TypeScript 型安全なフィーチャーフラグ設計:判別可能共用体で運用事故を防ぐ
フィーチャーフラグは新機能のロールアウトや A/B テストに欠かせない仕組みですが、実装を間違えると本番環境で予期せぬ挙動を引き起こす可能性があります。特に複数の機能バリエーションを管理する際、型の不整合が原因で「このフラグが有効なのに、必要なパラメータが渡されていない」といった運用事故が発生しがちです。
TypeScript の判別可能共用体(Discriminated Unions)を活用すれば、フィーチャーフラグの状態とそれに紐づく設定値を型レベルで結びつけられます。これにより、コンパイル時に不整合を検出でき、安全で保守性の高いフィーチャーフラグシステムを構築できるのです。
背景
フィーチャーフラグの役割
フィーチャーフラグは、コードをデプロイせずに機能のオン・オフを切り替える仕組みです。新機能を段階的にリリースしたり、特定のユーザーグループだけに公開したりする際に活用されます。
mermaidflowchart LR
user["ユーザー"] -->|リクエスト| app["アプリケーション"]
app -->|フラグ確認| flag["フィーチャーフラグ<br/>システム"]
flag -->|有効| featureA["新機能 A"]
flag -->|無効| featureB["既存機能 B"]
featureA -->|レスポンス| user
featureB -->|レスポンス| user
この図では、ユーザーのリクエストに対してフィーチャーフラグシステムが判断を行い、有効・無効に応じて異なる機能を提供する基本的なフローを示しています。
| # | 項目 | 説明 |
|---|---|---|
| 1 | カナリアリリース | 一部のユーザーにのみ新機能を公開し、問題がないか検証する |
| 2 | A/B テスト | 複数のバリエーションを用意し、ユーザーごとに異なる体験を提供する |
| 3 | 緊急停止 | 問題が発生した際、デプロイせずに即座に機能を無効化する |
| 4 | 段階的ロールアウト | パーセンテージ指定で徐々にユーザー範囲を拡大する |
従来の実装方法と課題
一般的なフィーチャーフラグの実装では、boolean 値や文字列を使って機能の有効・無効を判定します。
typescript// 従来の実装例:シンプルなboolean型
interface FeatureFlags {
enableNewUI: boolean;
enableBetaFeature: boolean;
}
しかし、このシンプルな実装では以下のような課題が発生します。
typescript// フラグとパラメータの関連性が型で保証されない
const flags = {
enableNewUI: true,
// rolloutPercentageが必要だが、型で強制されない
};
function renderUI(flags: FeatureFlags) {
if (flags.enableNewUI) {
// rolloutPercentageが存在するか実行時まで分からない
const percentage = (flags as any).rolloutPercentage;
}
}
型の不整合により、以下のような運用事故が発生する可能性があります。
- フラグが有効なのに必要なパラメータが渡されていない
- 無効なフラグに対して不要なパラメータがセットされている
- コードレビューで気づけず、本番環境でエラーが発生する
課題
型安全性の欠如がもたらす問題
従来のフィーチャーフラグ実装では、フラグの状態と関連パラメータが型レベルで結びついていません。
typescript// 問題のある実装例
interface UnsafeFeatureConfig {
enabled: boolean;
variant?: 'A' | 'B' | 'C';
rolloutPercentage?: number;
experimentId?: string;
}
この実装では、全てのプロパティがオプショナルになっており、以下のような不整合が型チェックをすり抜けてしまいます。
typescript// コンパイルエラーにならないが、実行時に問題が発生する例
const config1: UnsafeFeatureConfig = {
enabled: true,
// variantが必要なのに指定されていない
};
const config2: UnsafeFeatureConfig = {
enabled: false,
variant: 'A', // 無効なのにvariantが指定されている
rolloutPercentage: 50, // 無効なのにrolloutPercentageも指定
};
このような実装では、コンパイル時にエラーが発生せず、実行時に初めて問題が明らかになります。
実行時エラーのリスク
型の不整合が原因で、本番環境で以下のようなエラーが発生します。
typescript// 実行時エラーの例
function getVariant(config: UnsafeFeatureConfig): string {
if (config.enabled) {
// variantが存在しない場合、undefinedになる
return config.variant.toUpperCase(); // TypeError: Cannot read property 'toUpperCase' of undefined
}
return 'default';
}
| # | エラーの種類 | 発生条件 | 影響範囲 |
|---|---|---|---|
| 1 | TypeError | 必須パラメータが未定義 | ユーザー体験の破綻 |
| 2 | ロジックエラー | 無効なフラグで処理が実行される | データの不整合 |
| 3 | セキュリティリスク | アクセス制御の判定ミス | 未公開機能の漏洩 |
mermaidflowchart TD
start["フラグ取得"] --> check{enabled?}
check -->|true| getParam["パラメータ取得"]
getParam --> exists{パラメータ<br/>存在?}
exists -->|Yes| process["正常処理"]
exists -->|No| error["TypeError発生"]
check -->|false| skip["処理スキップ"]
error -->|"実行時エラー"| crash["アプリケーション<br/>クラッシュ"]
この図は、型安全性が欠如した実装で実行時エラーが発生するまでのフローを示しています。パラメータの存在確認が実行時まで行われないため、エラーが検出されるのが遅れます。
保守性の低下
フィーチャーフラグが増えるにつれて、どのフラグにどのパラメータが必要か把握するのが困難になります。
typescript// 10個以上のフラグと複雑な依存関係
interface ComplexFeatureFlags {
enableFeatureA: boolean;
featureAConfig?: { timeout: number };
enableFeatureB: boolean;
featureBVariant?: 'v1' | 'v2';
enableFeatureC: boolean;
featureCExperimentId?: string;
// ... さらに増え続ける
}
この状態では、新しいメンバーがコードを理解するのに時間がかかり、変更時のミスも増加します。
解決策
判別可能共用体(Discriminated Unions)とは
判別可能共用体は、TypeScript の型システムを活用して、複数の異なる型を 1 つの型として扱う手法です。共通のプロパティ(判別プロパティ)を持つことで、TypeScript が型を自動的に絞り込みます。
typescript// 判別可能共用体の基本構造
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number };
kindプロパティが判別プロパティとして機能し、TypeScript はこの値に基づいて型を推論します。
typescript// TypeScriptが型を自動的に絞り込む
function getArea(shape: Shape): number {
if (shape.kind === 'circle') {
// この分岐内では shape は { kind: "circle"; radius: number } 型
return Math.PI * shape.radius ** 2;
} else {
// この分岐内では shape は { kind: "rectangle"; width: number; height: number } 型
return shape.width * shape.height;
}
}
この仕組みをフィーチャーフラグ設計に応用すると、フラグの状態とパラメータを型レベルで結びつけられます。
型安全なフィーチャーフラグの設計
判別可能共用体を使って、フィーチャーフラグの各状態を個別の型として定義します。
typescript// 基本的な型定義:判別プロパティに enabled を使用
type FeatureFlag =
| { enabled: false }
| {
enabled: true;
variant: 'A' | 'B' | 'C';
rolloutPercentage: number;
};
この定義により、enabledがtrueの場合は必ずvariantとrolloutPercentageが存在することが型で保証されます。
typescript// コンパイルエラーになる例(型安全)
const invalidConfig1: FeatureFlag = {
enabled: true,
// Error: Property 'variant' is missing
};
const invalidConfig2: FeatureFlag = {
enabled: false,
variant: 'A', // Error: Property 'variant' does not exist on type '{ enabled: false }'
};
逆に、正しい定義はコンパイルエラーなく作成できます。
typescript// 正しい定義例
const validConfig1: FeatureFlag = {
enabled: false,
};
const validConfig2: FeatureFlag = {
enabled: true,
variant: 'B',
rolloutPercentage: 30,
};
型ガードによる安全なアクセス
判別可能共用体を使うと、TypeScript が自動的に型を絞り込むため、安全にプロパティへアクセスできます。
typescript// 型ガードを活用した実装
function handleFeature(flag: FeatureFlag): void {
if (flag.enabled) {
// この分岐内では flag の型が自動的に絞り込まれる
// flag: { enabled: true; variant: "A" | "B" | "C"; rolloutPercentage: number }
console.log(`Variant: ${flag.variant}`);
console.log(`Rollout: ${flag.rolloutPercentage}%`);
} else {
// この分岐内では flag の型は { enabled: false }
console.log('Feature is disabled');
// console.log(flag.variant); // Error: Property 'variant' does not exist
}
}
この仕組みにより、実行時エラーを防げます。
mermaidflowchart TD
start["フラグ取得<br/>FeatureFlag型"] --> check{enabled?}
check -->|true| narrow1["型が自動絞り込み<br/>variant & rollout<br/>確実に存在"]
check -->|false| narrow2["型が自動絞り込み<br/>enabled: false型"]
narrow1 --> safe1["安全にアクセス<br/>コンパイル時保証"]
narrow2 --> safe2["variantへの<br/>アクセス不可<br/>コンパイルエラー"]
safe1 --> result["実行時エラー無し"]
safe2 --> result
この図は、判別可能共用体を使った場合の型の絞り込みと安全性の仕組みを示しています。コンパイル時に型が保証されるため、実行時エラーが発生しません。
複数パターンへの拡張
実際のプロジェクトでは、さらに複雑なフィーチャーフラグが必要になることがあります。
typescript// より複雑なフィーチャーフラグの型定義
type AdvancedFeatureFlag =
| { status: 'disabled' }
| { status: 'enabled'; mode: 'simple' }
| {
status: 'enabled';
mode: 'ab-test';
variants: ['A', 'B'];
experimentId: string;
}
| {
status: 'enabled';
mode: 'rollout';
percentage: number;
targetAudience: string[];
};
この定義では、statusとmodeを判別プロパティとして使い、それぞれの状態で必要なパラメータが異なります。
typescript// パターンごとの処理
function processFeature(flag: AdvancedFeatureFlag): void {
switch (flag.status) {
case 'disabled':
console.log('Feature is disabled');
break;
case 'enabled':
// さらに mode で分岐
switch (flag.mode) {
case 'simple':
console.log('Simple mode enabled');
break;
case 'ab-test':
// experimentId と variants が確実に存在
console.log(`A/B Test: ${flag.experimentId}`);
console.log(
`Variants: ${flag.variants.join(', ')}`
);
break;
case 'rollout':
// percentage と targetAudience が確実に存在
console.log(`Rollout: ${flag.percentage}%`);
console.log(
`Target: ${flag.targetAudience.join(', ')}`
);
break;
}
break;
}
}
この実装により、どのパターンでも必要なパラメータが型レベルで保証されます。
| # | ステータス | モード | 必須パラメータ |
|---|---|---|---|
| 1 | disabled | - | なし |
| 2 | enabled | simple | なし |
| 3 | enabled | ab-test | variants, experimentId |
| 4 | enabled | rollout | percentage, targetAudience |
具体例
実践的なフィーチャーフラグシステムの実装
実際のプロジェクトで使える、型安全なフィーチャーフラグシステムを構築します。
ステップ 1:型定義の作成
まず、フィーチャーフラグの型を定義します。
typescript// features.types.ts
// フィーチャーフラグの基本型定義
export type FeatureFlagStatus =
| 'disabled'
| 'enabled'
| 'beta'
| 'deprecated';
// 個別機能ごとの型定義
export type NewUIFeature =
| { status: 'disabled' }
| {
status: 'enabled';
theme: 'light' | 'dark';
animationEnabled: boolean;
};
export type PaymentFeature =
| { status: 'disabled' }
| {
status: 'beta';
allowedUsers: string[];
testMode: boolean;
}
| {
status: 'enabled';
providers: string[];
defaultProvider: string;
};
この型定義により、各フィーチャーフラグの状態とそれに紐づくパラメータが明確になります。
ステップ 2:フラグ管理システムの実装
次に、フラグを管理するクラスを実装します。
typescript// featureFlags.ts
import {
NewUIFeature,
PaymentFeature,
} from './features.types';
// フィーチャーフラグの集約型
export interface FeatureFlags {
newUI: NewUIFeature;
payment: PaymentFeature;
}
フラグを取得・更新するための型安全なマネージャークラスを作成します。
typescript// フィーチャーフラグマネージャークラス
export class FeatureFlagManager {
private flags: FeatureFlags;
constructor(initialFlags: FeatureFlags) {
this.flags = initialFlags;
}
// 型安全なゲッター
getNewUIFeature(): NewUIFeature {
return this.flags.newUI;
}
getPaymentFeature(): PaymentFeature {
return this.flags.payment;
}
}
このマネージャークラスにより、フラグへのアクセスが型安全になります。
ステップ 3:フラグを使った処理の実装
実際にフラグを使って機能を切り替える処理を実装します。
typescript// ui.renderer.ts
import { NewUIFeature } from './features.types';
// 新UIの描画処理
export function renderUI(feature: NewUIFeature): void {
if (feature.status === 'disabled') {
// 旧UIを描画
renderLegacyUI();
return;
}
// この分岐では theme と animationEnabled が確実に存在
const theme = feature.theme;
const animated = feature.animationEnabled;
renderNewUI(theme, animated);
}
型の絞り込みにより、themeとanimationEnabledへのアクセスが安全に行えます。
typescript// 実装の詳細
function renderNewUI(
theme: 'light' | 'dark',
animated: boolean
): void {
console.log(`Rendering new UI with theme: ${theme}`);
if (animated) {
console.log('Animations enabled');
}
}
function renderLegacyUI(): void {
console.log('Rendering legacy UI');
}
ステップ 4:決済機能の実装
より複雑な決済機能のフラグ処理を実装します。
typescript// payment.handler.ts
import { PaymentFeature } from './features.types';
// 決済処理のハンドラー
export function processPayment(
feature: PaymentFeature,
userId: string,
amount: number
): void {
switch (feature.status) {
case 'disabled':
throw new Error(
'Payment feature is currently disabled'
);
case 'beta':
// allowedUsers と testMode が確実に存在
if (!feature.allowedUsers.includes(userId)) {
throw new Error(
'User not allowed for beta testing'
);
}
if (feature.testMode) {
console.log(
`[TEST MODE] Processing payment of ${amount}`
);
}
break;
case 'enabled':
// providers と defaultProvider が確実に存在
const provider = feature.defaultProvider;
console.log(`Processing payment via ${provider}`);
break;
}
}
この実装では、各状態で必要なパラメータが型レベルで保証されています。
mermaidstateDiagram-v2
[*] --> disabled
disabled --> beta: ベータ公開
beta --> enabled: 全体公開
enabled --> disabled: 緊急停止
beta --> disabled: 問題発覚
state beta {
[*] --> CheckUser
CheckUser --> AllowedUser: ユーザー確認
CheckUser --> Reject: 許可なし
AllowedUser --> TestMode: テストモード実行
}
state enabled {
[*] --> SelectProvider
SelectProvider --> ProcessPayment: プロバイダー選択
}
この状態遷移図は、決済機能のフラグが持つ異なる状態間の遷移と、各状態での処理フローを示しています。
ステップ 5:設定の初期化
実際の使用例として、フラグの初期化と利用方法を示します。
typescript// app.ts
import { FeatureFlagManager } from './featureFlags';
import { renderUI } from './ui.renderer';
import { processPayment } from './payment.handler';
// フラグの初期設定
const manager = new FeatureFlagManager({
newUI: {
status: 'enabled',
theme: 'dark',
animationEnabled: true,
},
payment: {
status: 'beta',
allowedUsers: ['user123', 'user456'],
testMode: true,
},
});
この設定では、新 UI は有効で、決済機能はベータテスト中という状態を表現しています。
typescript// フラグを使った処理の実行
export function initializeApp(): void {
// UI の描画
const uiFeature = manager.getNewUIFeature();
renderUI(uiFeature);
// 決済処理の実行例
const paymentFeature = manager.getPaymentFeature();
try {
processPayment(paymentFeature, 'user123', 1000);
} catch (error) {
console.error('Payment failed:', error);
}
}
型安全性により、実行時エラーを大幅に削減できます。
リモート設定との統合
実際のプロダクトでは、フラグの設定を外部サービスから取得することが一般的です。
typescript// remote.config.ts
import { FeatureFlags } from './featureFlags';
// リモートAPIからのレスポンス型
interface RemoteConfigResponse {
newUI: {
status: string;
theme?: string;
animationEnabled?: boolean;
};
payment: {
status: string;
allowedUsers?: string[];
testMode?: boolean;
providers?: string[];
defaultProvider?: string;
};
}
リモートから取得したデータを型安全な形式に変換する関数を実装します。
typescript// リモート設定のパース処理
export function parseRemoteConfig(
response: RemoteConfigResponse
): FeatureFlags {
return {
newUI: parseNewUIFeature(response.newUI),
payment: parsePaymentFeature(response.payment),
};
}
個別のパース関数を実装し、型の整合性を保証します。
typescript// 新UI機能のパース
function parseNewUIFeature(
data: RemoteConfigResponse['newUI']
): NewUIFeature {
if (data.status === 'disabled') {
return { status: 'disabled' };
}
if (
data.status === 'enabled' &&
data.theme &&
typeof data.animationEnabled === 'boolean'
) {
return {
status: 'enabled',
theme: data.theme as 'light' | 'dark',
animationEnabled: data.animationEnabled,
};
}
// デフォルト値を返す
return { status: 'disabled' };
}
この実装により、外部データの不整合をパース段階で吸収し、アプリケーション内では常に型安全な状態を保てます。
typescript// 決済機能のパース
function parsePaymentFeature(
data: RemoteConfigResponse['payment']
): PaymentFeature {
switch (data.status) {
case 'beta':
if (
data.allowedUsers &&
typeof data.testMode === 'boolean'
) {
return {
status: 'beta',
allowedUsers: data.allowedUsers,
testMode: data.testMode,
};
}
break;
case 'enabled':
if (data.providers && data.defaultProvider) {
return {
status: 'enabled',
providers: data.providers,
defaultProvider: data.defaultProvider,
};
}
break;
}
return { status: 'disabled' };
}
パース処理により、外部 API の変更や不正なデータからアプリケーションを保護できます。
テストコードの実装
型安全なフィーチャーフラグは、テストも容易になります。
typescript// featureFlags.test.ts
import { renderUI } from './ui.renderer';
import { processPayment } from './payment.handler';
import {
NewUIFeature,
PaymentFeature,
} from './features.types';
describe('Feature Flags', () => {
describe('NewUI Feature', () => {
test('disabled状態では旧UIが描画される', () => {
const feature: NewUIFeature = { status: 'disabled' };
// renderUI関数が型安全に呼び出せる
expect(() => renderUI(feature)).not.toThrow();
});
});
});
判別可能共用体により、テストケースの網羅性も確保しやすくなります。
typescript// すべての状態をテスト
describe('Payment Feature', () => {
test('disabled状態では決済が拒否される', () => {
const feature: PaymentFeature = { status: 'disabled' };
expect(() =>
processPayment(feature, 'user123', 1000)
).toThrow();
});
test('beta状態では許可ユーザーのみ決済可能', () => {
const feature: PaymentFeature = {
status: 'beta',
allowedUsers: ['user123'],
testMode: true,
};
expect(() =>
processPayment(feature, 'user123', 1000)
).not.toThrow();
expect(() =>
processPayment(feature, 'user999', 1000)
).toThrow();
});
test('enabled状態では全ユーザーが決済可能', () => {
const feature: PaymentFeature = {
status: 'enabled',
providers: ['stripe', 'paypal'],
defaultProvider: 'stripe',
};
expect(() =>
processPayment(feature, 'any-user', 1000)
).not.toThrow();
});
});
型システムがテストケースの漏れを防ぎ、リファクタリング時の安全性も向上します。
| # | テスト項目 | 期待動作 | カバレッジ |
|---|---|---|---|
| 1 | disabled 状態 | 機能が無効 | 基本パス |
| 2 | beta 状態(許可ユーザー) | 機能が有効 | 正常系 |
| 3 | beta 状態(非許可ユーザー) | エラー発生 | 異常系 |
| 4 | enabled 状態 | 全ユーザーで有効 | 基本パス |
まとめ
TypeScript の判別可能共用体を活用することで、フィーチャーフラグシステムの型安全性を大幅に向上できます。この手法により、フラグの状態と必要なパラメータを型レベルで結びつけ、コンパイル時に不整合を検出できるようになりました。
従来の実装では実行時まで発見できなかったエラーを、コンパイル時に防ぐことができます。これにより、本番環境での運用事故リスクが大幅に減少し、開発者の認知負荷も軽減されます。
型安全なフィーチャーフラグ設計は、複雑な機能のロールアウトや A/B テストを安全に実行するための強力な基盤となります。チームの規模が大きくなるほど、この設計の恩恵は大きくなるでしょう。
プロジェクトに導入する際は、まず既存のフラグから 1 つずつ判別可能共用体へ移行し、段階的に適用範囲を広げることをおすすめします。型定義を整備することで、長期的な保守性と開発効率が向上するはずです。
関連リンク
articleTypeScript 型安全なフィーチャーフラグ設計:判別可能共用体で運用事故を防ぐ
articleTypeScript satisfies 演算子の実力:型の過剰/不足を一発検知する実践ガイド
articlePlaywright × TypeScript 超入門チュートリアル:型安全 E2E を最短構築
articleTypeScript 型カバレッジを KPI 化:`type-coverage`でチームの型品質を可視化する
articleTypeScript 公開 API の型設計術:`export type`/`interface`/`class`の責務分担と境界設計
articleESLint を Yarn + TypeScript + React でゼロから構築:Flat Config 完全手順(macOS)
articleWebRTC が「connecting」のまま進まない:ICE 失敗を 5 分で切り分ける手順
articleWeb Components が “is not a constructor” で落ちる時:定義順序と複数登録の衝突を解決
articleVitest モジュールモック技術の基礎と応用:`vi.mock` / `vi.spyOn` を極める
articleVue.js リアクティビティ内部解剖:Proxy/ref/computed を図で読み解く
articleVite CSS HMR が反映されない時のチェックリスト:PostCSS/Modules/Cache 編
articleTailwind CSS 2025 年ロードマップ総ざらい:新機能・互換性・移行の見取り図
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来