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 へ移行し、段階的に適用範囲を広げることをおすすめします。
最初は複雑な状態を持つフラグ(決済、認証など)から始め、効果を検証してから他のフラグへ展開すると良いでしょう。型定義を整備することで、長期的な保守性と開発効率が向上するはずです。
実運用では、リモート設定のパース処理でバリデーションとロギングを追加し、不正なデータが混入した際の早期検出体制を整えることも重要です。
関連リンク
著書
article2026年1月23日TypeScriptのtypeとinterfaceを比較・検証する 違いと使い分けの判断基準を整理
article2026年1月23日TypeScript 5.8の型推論を比較・検証する 強化点と落とし穴の回避策
article2026年1月23日TypeScript Genericsの使用例を早見表でまとめる 記法と頻出パターンを整理
article2026年1月22日TypeScriptの型システムを概要で理解する 基礎から全体像まで完全解説
article2026年1月22日ZustandとTypeScriptのユースケース ストアを型安全に設計して運用する実践
article2026年1月22日TypeScriptでよく出るエラーをトラブルシュートでまとめる 原因と解決法30選
articleshadcn/ui × TanStack Table 設計術:仮想化・列リサイズ・アクセシブルなグリッド
articleRemix のデータ境界設計:Loader・Action とクライアントコードの責務分離
articlePreact コンポーネント設計 7 原則:再レンダリング最小化の分割と型付け
articlePHP 8.3 の新機能まとめ:readonly クラス・型強化・性能改善を一気に理解
articleNotebookLM に PDF/Google ドキュメント/URL を取り込む手順と最適化
articlePlaywright 並列実行設計:shard/grep/fixtures で高速化するテストスイート設計術
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 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
