TypeScriptでフィーチャーフラグを安全に運用する設計 判別可能unionで事故を防ぐ
フィーチャーフラグで新機能のロールアウトや A/B テストを管理していると、「このフラグが有効なのに必要なパラメータが渡されていない」という運用事故に遭遇したことはないでしょうか。
実際に業務でフィーチャーフラグシステムを運用してきた中で、boolean 型だけの単純な実装では型の不整合が原因でバグが混入しやすく、コードレビューでも見落とされがちでした。特に複数の機能バリエーションを管理する際、実行時まで不整合が発覚しないケースが多発していたのです。
この記事では、TypeScript の 判別可能 union(Discriminated Unions) を使い、フラグの状態とパラメータを型レベルで結びつけることで、コンパイル時に不整合を検出する設計を解説します。実際のプロジェクトで採用した設計パターンと、運用で学んだ注意点を共有しますので、型安全なフィーチャーフラグシステムを構築したいエンジニアの参考になれば幸いです。
検証環境
本記事では、以下の環境で動作確認を行っています。
- OS: macOS Sequoia 15.2
- Node.js: 22.12.0(LTS)
- 主要パッケージ:
- TypeScript: 5.7.2
- Next.js: 15.1.0
- React: 19.0.0
- Jest: 29.7.0
- 検証日: 2025 年 12 月 24 日
フィーチャーフラグ運用で直面した型安全性の課題
プロダクション環境でのフラグ活用シーン
実務でフィーチャーフラグを導入する際、主に以下のようなシナリオで活用されます。
| # | ユースケース | 目的 | 典型的な実装パターン |
|---|---|---|---|
| 1 | カナリアリリース | 一部ユーザーで新機能を検証 | ユーザー ID/グループでの絞り込み |
| 2 | A/B テスト | 複数バリエーションの効果測定 | ランダム振り分け+実験 ID 管理 |
| 3 | 緊急停止スイッチ | 障害時の即座な機能無効化 | リモート設定との連携 |
| 4 | 段階的ロールアウト | パーセンテージでの段階公開 | 確率ベースの判定処理 |
以下の図は、ユーザーリクエストに対してフィーチャーフラグシステムが判断を行い、状態に応じて異なる機能を提供する基本フローを示しています。
mermaidflowchart LR
user["ユーザー"] -->|リクエスト| app["アプリケーション"]
app -->|フラグ確認| flag["フィーチャーフラグ<br/>システム"]
flag -->|有効| featureA["新機能 A"]
flag -->|無効| featureB["既存機能 B"]
featureA -->|レスポンス| user
featureB -->|レスポンス| user
フラグ状態による分岐処理が適切に実装されることで、デプロイせずに機能の切り替えが可能になります。
boolean 型ベースの実装で遭遇した実行時エラー
初期のフィーチャーフラグ実装では、シンプルに boolean 型で管理していました。
以下は、実際のプロジェクトで使っていた初期実装です。
typescript// 初期の型定義:boolean型で管理
interface FeatureFlags {
enableNewUI: boolean;
enableBetaPayment: boolean;
}
この定義では、フラグの有効/無効しか表現できません。
実際の業務では、フラグに付随する設定値が必要なケースが頻繁にありました。
typescript// フラグに関連するパラメータを別途定義
interface FeatureFlagConfig {
flags: FeatureFlags;
rolloutPercentage?: number;
experimentId?: string;
}
しかし、この構造では型の不整合が発生しやすくなります。
typescript// コンパイルは通るが、実行時にエラーが発生する例
const config: FeatureFlagConfig = {
flags: {
enableNewUI: true, // フラグは有効
enableBetaPayment: false,
},
// rolloutPercentageが必要なのに未定義
};
function renderUI(config: FeatureFlagConfig) {
if (config.flags.enableNewUI) {
// rolloutPercentageがundefinedになる
const percentage = config.rolloutPercentage!;
// 実行時に想定外の動作が発生
}
}
実際にこのコードで本番環境にデプロイしたところ、以下のようなエラーログが記録されました。
bashTypeError: Cannot read property 'toFixed' of undefined
at renderUI (ui.renderer.ts:42)
at processRequest (app.ts:128)
このエラーは、フラグが有効なのにrolloutPercentageが渡されていなかったために発生しました。
コードレビューでも見落とされる型の不整合
実務では、以下のような不整合がコードレビューをすり抜けてしまうケースがありました。
typescript// レビューで見落とされた問題コード
const invalidConfig: FeatureFlagConfig = {
flags: {
enableNewUI: false, // フラグは無効
enableBetaPayment: false,
},
rolloutPercentage: 50, // 無効なのにパラメータを設定
experimentId: "exp-001", // 使われないパラメータ
};
TypeScript の型チェックではエラーにならないため、レビュアーも気づきにくい問題でした。
この状態でコードが運用されると、以下のような問題が発生します。
| # | 問題の種類 | 発生条件 | 影響範囲 |
|---|---|---|---|
| 1 | TypeError | 必須パラメータが未定義 | ユーザー体験の破綻 |
| 2 | ロジックエラー | 無効なフラグで処理が実行 | データの不整合 |
| 3 | セキュリティリスク | アクセス制御の判定ミス | 未公開機能の漏洩 |
| 4 | デバッグ困難 | 実行時まで問題が顕在化しない | 調査コストの増大 |
判別可能 union による型の不整合を防ぐ設計判断
TypeScript の union 型と型ガードの仕組み
判別可能 union(Discriminated Unions)は、TypeScript の型システムを活用して、複数の異なる型を 1 つの型として扱う手法です。
ユニオン型の基本構造として、共通のプロパティ(判別プロパティ)を持つことで、TypeScript が型ガードにより型を自動的に絞り込みます。
以下は、判別可能 union の基本的な例です。
typescript// 判別プロパティとしてkindを使用
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number };
この定義により、kindの値に応じて異なるプロパティセットが要求されます。
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;
}
}
この型安全な仕組みをフィーチャーフラグ設計に応用することで、フラグの状態とパラメータを型レベルで結びつけられます。
フラグ状態とパラメータを紐付ける型定義
実際のプロジェクトで採用した、判別可能 union を使ったフィーチャーフラグの型定義を紹介します。
以下の型定義では、enabledを判別プロパティとして使用し、有効/無効で異なるプロパティセットを定義しています。
typescript// 判別可能unionによるフィーチャーフラグの型定義
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 in type
// '{ enabled: true; }' but required in type
// '{ enabled: true; variant: "A" | "B" | "C"; rolloutPercentage: number; }'
};
無効なフラグに不要なパラメータを設定した場合もエラーになります。
typescript// これもコンパイルエラー
const invalidConfig2: FeatureFlag = {
enabled: false,
variant: "A",
// Error: Object literal may only specify known properties,
// and 'variant' does not exist in type '{ enabled: false; }'
};
正しい定義のみがコンパイルを通過します。
typescript// 正しい定義例
const validConfig1: FeatureFlag = {
enabled: false,
};
const validConfig2: FeatureFlag = {
enabled: true,
variant: "B",
rolloutPercentage: 30,
};
型ガードによる安全なプロパティアクセス
判別可能 union を使うと、条件分岐内で TypeScript が自動的に型を絞り込むため、安全にプロパティへアクセスできます。
以下の実装では、if (flag.enabled)の分岐内で型が自動的に絞り込まれています。
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
}
}
実行時エラーを防ぐために、型ガードが重要な役割を果たします。
以下の図は、判別可能 union と型ガードによる型の絞り込みと安全性の仕組みを示しています。
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
コンパイル時に型が保証されるため、実行時エラーが発生しなくなります。この型安全性により、boolean 型ベースの実装で遭遇していた運用事故を防げるようになりました。
複数状態を持つフラグへの拡張設計
実際の業務では、無効/有効の 2 状態だけでなく、より複雑な状態管理が必要になることがあります。
以下は、実プロジェクトで採用した、複数の状態とモードを持つフィーチャーフラグの型定義です。
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を判別プロパティとして使い、それぞれの状態で必要なパラメータが異なります。
各パターンに対応した処理を実装する際、switch 文で分岐します。
typescript// パターンごとの処理実装
function processFeature(flag: AdvancedFeatureFlag): void {
switch (flag.status) {
case "disabled":
console.log("Feature is disabled");
break;
case "enabled":
// さらに mode で分岐
handleEnabledFeature(flag);
break;
}
}
有効な状態では、さらにモードごとに処理を分岐します。
typescript// 有効状態のモード別処理
function handleEnabledFeature(
flag: Extract<AdvancedFeatureFlag, { status: "enabled" }>
): void {
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;
}
}
この実装により、どのパターンでも必要なパラメータが型レベルで保証されます。
各状態と必要なパラメータの対応関係を表にまとめると、以下のようになります。
| # | ステータス | モード | 必須パラメータ | 用途 |
|---|---|---|---|---|
| 1 | disabled | - | なし | 機能の完全停止 |
| 2 | enabled | simple | なし | 単純な有効化 |
| 3 | enabled | ab-test | variants, experimentId | A/B テスト実施 |
| 4 | enabled | rollout | percentage, targetAudience | 段階的ロールアウト |
型安全なフィーチャーフラグシステムの実装手順
ステップ 1:機能ごとの型定義ファイル作成
実際のプロジェクトで使える、型安全なフィーチャーフラグシステムを構築していきます。
まず、フィーチャーフラグの基本的な状態を定義します。
typescript// features.types.ts
// フィーチャーフラグで使用する状態の型定義
export type FeatureFlagStatus = "disabled" | "enabled" | "beta" | "deprecated";
新 UI フィーチャーの型定義を作成します。無効状態と有効状態で異なるプロパティセットを定義しています。
typescript// 新UI機能のフィーチャーフラグ型
export type NewUIFeature =
| { status: "disabled" }
| {
status: "enabled";
theme: "light" | "dark";
animationEnabled: boolean;
};
決済機能のフィーチャーフラグは、さらに複雑な状態遷移を持ちます。
typescript// 決済機能のフィーチャーフラグ型
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;
}
// 型安全なゲッター:NewUIFeature型を返す
getNewUIFeature(): NewUIFeature {
return this.flags.newUI;
}
// 型安全なゲッター:PaymentFeature型を返す
getPaymentFeature(): PaymentFeature {
return this.flags.payment;
}
}
このマネージャークラスにより、フラグへのアクセスが型安全になり、誤った型のフラグを取得することを防げます。
フラグを更新するメソッドも型安全に実装できます。
typescript// フラグの更新メソッド
export class FeatureFlagManager {
// ... 前述のコード
// 新UIフラグの更新
updateNewUIFeature(feature: NewUIFeature): void {
this.flags.newUI = feature;
}
// 決済フラグの更新
updatePaymentFeature(feature: PaymentFeature): void {
this.flags.payment = feature;
}
}
ステップ 3:UI 描画処理への適用実装
新 UI の描画処理に、型安全なフィーチャーフラグを適用します。
以下の実装では、フラグの状態に応じて異なる UI 描画処理を呼び出しています。
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);
}
新 UI の描画処理の詳細実装です。
typescript// 新UI描画の実装詳細
function renderNewUI(theme: "light" | "dark", animated: boolean): void {
console.log(`Rendering new UI with theme: ${theme}`);
if (animated) {
console.log("Animations enabled");
// アニメーション付きUI描画処理
} else {
// 静的UI描画処理
}
}
旧 UI の描画処理も定義しています。
typescript// 旧UI描画処理
function renderLegacyUI(): void {
console.log("Rendering legacy UI");
// 既存のUI描画処理
}
型の絞り込みにより、themeとanimationEnabledへのアクセスが安全に行えることを確認しました。
ステップ 4:決済処理の複雑な状態管理実装
決済機能では、3 つの状態(disabled、beta、enabled)を持ち、それぞれで異なる処理を実装します。
以下は、決済処理のハンドラー実装です。
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":
handleBetaPayment(feature, userId, amount);
break;
case "enabled":
handleEnabledPayment(feature, userId, amount);
break;
}
}
beta 状態での決済処理実装です。許可ユーザーのチェックとテストモードの判定を行います。
typescript// beta状態の決済処理
function handleBetaPayment(
feature: Extract<PaymentFeature, { status: "beta" }>,
userId: string,
amount: number
): void {
// 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}`);
} else {
console.log(`[BETA] Processing payment of ${amount}`);
}
}
enabled 状態での決済処理実装です。プロバイダー選択と実際の決済処理を行います。
typescript// enabled状態の決済処理
function handleEnabledPayment(
feature: Extract<PaymentFeature, { status: "enabled" }>,
userId: string,
amount: number
): void {
// providers と defaultProvider が確実に存在
const provider = feature.defaultProvider;
console.log(`Processing payment via ${provider}`);
console.log(`Available providers: ${feature.providers.join(", ")}`);
}
この実装では、各状態で必要なパラメータが型レベルで保証されています。
以下の図は、決済機能のフラグが持つ異なる状態間の遷移と、各状態での処理フローを示しています。
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();
executePayment(paymentFeature);
}
エラーハンドリングを含めた決済処理の実行実装です。
typescript// エラーハンドリング付き決済実行
function executePayment(paymentFeature: PaymentFeature): void {
try {
processPayment(paymentFeature, "user123", 1000);
} catch (error) {
console.error("Payment failed:", error);
// エラー通知処理など
}
}
型安全性により、実行時エラーを大幅に削減できることを検証で確認しました。
リモート設定サービスとの型安全な統合
実際のプロダクトでは、フラグの設定を外部サービス(LaunchDarkly、Firebase Remote Config など)から取得することが一般的です。
リモート API からのレスポンス型を定義します。外部 API は型が保証されていないため、optional 型で定義しています。
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),
};
}
新 UI 機能のパース関数です。不正なデータの場合はデフォルト値を返します。
typescript// 新UI機能のパース実装
function parseNewUIFeature(data: RemoteConfigResponse["newUI"]): NewUIFeature {
if (data.status === "disabled") {
return { status: "disabled" };
}
// enabledの場合は必須パラメータをチェック
if (
data.status === "enabled" &&
data.theme &&
typeof data.animationEnabled === "boolean"
) {
// 型アサーションで型安全を保つ
const theme = data.theme as "light" | "dark";
return {
status: "enabled",
theme,
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 の変更や不正なデータからアプリケーションを保護できます。実運用では、このレイヤーでバリデーションとロギングを追加することをおすすめします。
実運用で発生したエラーと対処法
エラー 1: Type 'string' is not assignable to type '"light" | "dark"'
リモート設定から取得した theme 値をそのまま代入しようとした際に、以下のコンパイルエラーが発生しました。
typescript// エラーが発生したコード
function parseNewUIFeature(data: RemoteConfigResponse["newUI"]): NewUIFeature {
if (data.status === "enabled" && data.theme) {
return {
status: "enabled",
theme: data.theme, // Error: Type 'string' is not assignable to type '"light" | "dark"'
animationEnabled: data.animationEnabled ?? false,
};
}
return { status: "disabled" };
}
発生条件
- リモート API から取得した
string型の値を、ユニオン型のプロパティに直接代入した場合
原因
TypeScript はstring型を'light' | 'dark'型に自動的には変換しません。型アサーションまたは型ガードが必要です。
解決方法
型ガードで値をチェックしてから代入するか、型アサーションを使用します。
typescript// 修正後(型ガードを使用)
function parseNewUIFeature(data: RemoteConfigResponse["newUI"]): NewUIFeature {
if (data.status === "enabled" && data.theme) {
// 型ガードで有効な値かチェック
if (data.theme === "light" || data.theme === "dark") {
return {
status: "enabled",
theme: data.theme, // OK: 型が絞り込まれている
animationEnabled: data.animationEnabled ?? false,
};
}
}
return { status: "disabled" };
}
型アサーションを使う場合は、値の妥当性を事前に確認してください。
typescript// 型アサーションを使う方法(値の妥当性確認が必要)
const theme =
data.theme === "light" || data.theme === "dark"
? (data.theme as "light" | "dark")
: "light"; // デフォルト値
解決後の確認
修正後、リモート設定から不正な値('blue'など)が返されても、デフォルト値が使われることを確認しました。
参考リンク
エラー 2: Property 'variant' does not exist on type 'never'
switch 文で全てのケースを網羅していない場合に、以下のコンパイルエラーが発生しました。
typescript// エラーが発生したコード
function handleFeature(flag: AdvancedFeatureFlag): void {
switch (flag.status) {
case "disabled":
console.log("Disabled");
break;
// enabledケースの処理を忘れている
}
// この時点で flag は never 型になる
console.log(flag.variant); // Error: Property 'variant' does not exist on type 'never'
}
発生条件
- switch 文で全ての判別プロパティの値を網羅していない場合
- TypeScript が「ここには到達しない」と判断し、
never型を推論する
原因
型の絞り込みロジックが不完全で、全てのパターンをカバーしていないため。
解決方法 1:default 句を追加
default 句を追加して、全てのケースを網羅します。
typescript// 修正後(default句を追加)
function handleFeature(flag: AdvancedFeatureFlag): void {
switch (flag.status) {
case "disabled":
console.log("Disabled");
break;
case "enabled":
handleEnabledFeature(flag);
break;
default:
// 網羅性チェック:ここに到達したらコンパイルエラー
const _exhaustiveCheck: never = flag;
throw new Error(`Unhandled status: ${_exhaustiveCheck}`);
}
}
解決方法 2:型ガード関数を使用
型ガード関数を使って、明示的に型を絞り込みます。
typescript// 型ガード関数を使った実装
function isEnabled(
flag: AdvancedFeatureFlag
): flag is Extract<AdvancedFeatureFlag, { status: "enabled" }> {
return flag.status === "enabled";
}
function handleFeature(flag: AdvancedFeatureFlag): void {
if (isEnabled(flag)) {
handleEnabledFeature(flag);
} else {
console.log("Disabled");
}
}
解決後の確認
新しいステータスを追加した際に、switch 文の網羅性チェックが機能し、コンパイルエラーで気づけることを確認しました。
参考リンク
テストコードの実装と型安全性の恩恵
型安全なフィーチャーフラグは、テストも容易になります。
以下は、Jest を使ったテストコードの例です。
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" };
expect(() => renderUI(feature)).not.toThrow();
});
});
});
有効状態のテストケースです。型定義により、必須パラメータの記述漏れを防げます。
typescript// 有効状態のテスト
test("enabled状態では新UIが描画される", () => {
const feature: NewUIFeature = {
status: "enabled",
theme: "dark",
animationEnabled: true,
};
expect(() => renderUI(feature)).not.toThrow();
});
判別可能 union により、テストケースの網羅性も確保しやすくなります。
typescript// すべての状態をテスト
describe("Payment Feature", () => {
test("disabled状態では決済が拒否される", () => {
const feature: PaymentFeature = { status: "disabled" };
expect(() => processPayment(feature, "user123", 1000)).toThrow(
"Payment feature is currently disabled"
);
});
test("beta状態では許可ユーザーのみ決済可能", () => {
const feature: PaymentFeature = {
status: "beta",
allowedUsers: ["user123"],
testMode: true,
};
// 許可ユーザーは成功
expect(() => processPayment(feature, "user123", 1000)).not.toThrow();
// 非許可ユーザーは失敗
expect(() => processPayment(feature, "user999", 1000)).toThrow(
"User not allowed for beta testing"
);
});
});
enabled 状態のテストケースです。
typescript// enabled状態のテスト
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 状態 | 全ユーザーで有効 | 基本パス |
| 5 | enabled 状態(theme: light) | ライトテーマ描画 | バリエーション |
| 6 | enabled 状態(theme: dark) | ダークテーマ描画 | バリエーション |
実運用では、これらのテストを CI/CD パイプラインに組み込むことで、型の不整合を早期に検出できるようになりました。
条件付き結論:判別可能 union が有効なケース
TypeScript の判別可能 union を活用することで、フィーチャーフラグシステムの型安全性を大幅に向上できました。
この手法により、フラグの状態と必要なパラメータを型レベルで結びつけ、コンパイル時に不整合を検出できるようになります。従来の boolean 型ベースの実装では実行時まで発見できなかったエラーを、コンパイル時に防げることを実プロジェクトで検証しました。
この設計が向いているケース
実務で判別可能 union 設計を採用する際、以下のケースで特に効果を発揮します。
- フラグに紐づくパラメータが複数ある場合(rolloutPercentage、variant、experimentId など)
- フラグの状態が 3 つ以上ある場合(disabled、beta、enabled など)
- チームメンバーが多く、フラグの仕様把握が困難な場合
- リモート設定サービスと連携し、外部データの型安全性を保ちたい場合
- テストコードで状態の網羅性を保証したい場合
この設計が向かないケース
一方で、以下のケースでは過度な設計になる可能性があります。
- フラグが単純な boolean 型で十分な場合(パラメータが不要)
- プロトタイプや短期的なプロジェクトの場合
- チームが TypeScript の型システムに不慣れな場合
段階的導入のすすめ
プロジェクトに導入する際は、まず既存のフラグから 1 つずつ判別可能 union へ移行し、段階的に適用範囲を広げることをおすすめします。
最初は複雑な状態を持つフラグ(決済、認証など)から始め、効果を検証してから他のフラグへ展開すると良いでしょう。型定義を整備することで、長期的な保守性と開発効率が向上するはずです。
実運用では、リモート設定のパース処理でバリデーションとロギングを追加し、不正なデータが混入した際の早期検出体制を整えることも重要です。
関連リンク
著書
article2025年12月31日TypeScriptでUIコンポーネントを設計する 型から考えるPropsと責務分割
article2025年12月30日TypeScriptの型安全を壊すNGコードを概要でまとめる 回避策10選で負債を減らす
article2025年12月29日SvelteとTypeScriptのユースケース 型安全な開発スタイルを整理する
article2025年12月29日TypeScriptで環境変数を型安全に管理する使い方 envスキーマ設計と運用の基本
article2025年12月29日TypeScriptの型定義ファイルdtsを自作する使い方 破綻しない設計の基本
article2025年12月28日TypeScriptでResult型の使い方を整理する Neverthrowで型安全なエラーハンドリング
article2025年12月31日TypeScriptでUIコンポーネントを設計する 型から考えるPropsと責務分割
article2025年12月30日TypeScriptの型安全を壊すNGコードを概要でまとめる 回避策10選で負債を減らす
article2025年12月29日SvelteとTypeScriptのユースケース 型安全な開発スタイルを整理する
article2025年12月29日TypeScriptで環境変数を型安全に管理する使い方 envスキーマ設計と運用の基本
article2025年12月29日TypeScriptの型定義ファイルdtsを自作する使い方 破綻しない設計の基本
article2025年12月28日TypeScriptでResult型の使い方を整理する Neverthrowで型安全なエラーハンドリング
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 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
