Skip to Content
Course content

240: Typing Feature Flags Safely

Click on the "Edit" button in the top corner of the screen to edit your slide content.

We've all been there. You're pushing a high-risk feature to production, so you wrap it in a feature flag. It starts simple—maybe just a boolean in a config file or a value coming from a remote API like LaunchDarkly. But as the project grows, your feature flag logic usually evolves into a mess of "magic strings" scattered across the codebase. I've seen projects with hundreds of flags where nobody remembers what is_new_header_v2_final_updated actually does.

The danger of stringly-typed flags

The naive way to handle flags is to treat them as a generic dictionary. You'll often see something like this in the wild:

const flags: Record<string, boolean> = {
  'new-checkout-flow': true,
  'beta-search-bar': false,
};

if (flags['new-checkot-flow']) { // Typo here!
  renderNewCheckout();
}

The problem here is that TypeScript is doing exactly what you told it to: allowing any string as a key. In the example above, I misspelled checkout as checkot. TypeScript doesn't blink. At runtime, flags['new-checkot-flow'] returns undefined, which is falsy. The feature simply doesn't turn on, and you spend two hours debugging why your "enabled" flag isn't working, only to realize it was a typo. It's a frustrating waste of time.

Moving toward a single source of truth

To fix this, we need to stop treating our flags as a generic map and start treating them as a first-class type. By defining a strict interface for your flags, you shift the burden of correctness from your memory to the compiler.

interface FeatureFlags {
  newCheckoutFlow: boolean;
  betaSearchBar: boolean;
  experimentalTheme: 'light' | 'dark' | 'system'; 
}

const flags: FeatureFlags = {
  newCheckoutFlow: true,
  betaSearchBar: false,
  experimentalTheme: 'system',
};

// Now, this would be a compile-time error:
if (flags.newCheckotFlow) { 
  renderNewCheckout();
}

I noticed in the example above that I added experimentalTheme as a union of strings. This is where the typed approach really pays off. Real-world feature flags aren't always binary; sometimes they are multivariate. If you use a Record<string, boolean>, you're forced to create three different boolean flags (e.g., isThemeLight, isThemeDark) which is clumsy and error-prone. A union type makes the intent explicit.

Dealing with the "Zombie Flag" problem

The biggest cost of a strictly typed system isn't the initial setup—it's the cleanup. Feature flags are meant to be temporary. Once a feature is 100% rolled out, the flag should be deleted. When you use magic strings, finding every instance of 'new-checkout-flow' across a massive repo is a game of "grep and pray."

With a typed FeatureFlags interface, you have a central registry. When it's time to kill a flag, you delete the property from the interface. Immediately, TypeScript will light up your entire IDE with red squiggles everywhere that flag was being used. I actually love this part of the process; it transforms the tedious task of "hunting for dead code" into a simple checklist of compiler errors. You can't accidentally leave a zombie flag lurking in a forgotten utility file because the code simply won't compile until the reference is gone.




📋 Practical Task

Refactoring the Beta Dashboard Flag System

You've inherited a codebase where the feature flags are handled via a Record<string, any>, leading to several runtime bugs. Your task is to bring type safety to the system.

// CURRENT NAIVE IMPLEMENTATION
const flags: Record<string, any> = {
  'show-beta-dashboard': true,
  'api-version': 'v2',
  'enable-logging': false
};

function initializeApp() {
  // BUG: This typo causes the dashboard to never show, 
  // but TypeScript doesn't warn us.
  if (flags['show-beta-dashbord']) { 
    console.log("Loading Beta Dashboard...");
  }
  
  console.log(`Using API: ${flags['api-version']}`);
}

Your Goal:

  • Create a FeatureFlags interface that strictly defines the three flags present in the object.
  • Ensure api-version is restricted to specific allowed versions (e.g., 'v1' | 'v2') rather than a generic string.
  • Apply this interface to the flags constant.
  • Fix the typo in the initializeApp function so that the compiler helps you find and correct it.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.