Master TypeScript feature flags using const assertions and mapped types. Learn to enforce compile-time safety and eliminate runtime configuration bugs.
Last month, I spent about three hours debugging a production incident caused by a simple typo in a feature flag key. The flag was toggled off in our dashboard, but the application code was still trying to read a legacy string key that didn't exist anymore.
We’ve all been there. Using raw strings for feature flags is a recipe for silent, catastrophic failures. If you want to move beyond "hope-based development," you need to leverage TypeScript to make your Feature Flags strictly typed. By combining Const Assertions and Mapped Types, we can turn our configuration into a source of truth that the compiler actually understands.
In many older codebases, we treat flags as a loose dictionary:
TYPESCRIPTconst flags = { "new-checkout-flow": true, "beta-dashboard": false, }; // Somewhere in the app if (flags["new-checkout-flow"]) { // ... }
This is brittle. If you rename a key or delete a flag, TypeScript won't warn you. You'll only find out when the app crashes or behaves unexpectedly in production. To fix this, we first need to lock down our configuration object.
The first step toward Type Safety is ensuring our configuration object is read-only and its values are literal types, not just generic string or boolean. We use as const for this.
TYPESCRIPTconst featureFlags = { enableNewCheckout: true, enableBetaDashboard: false, maxItemsInCart: 5, } as const;
By adding as const, TypeScript treats this object as a read-only set of literal values. enableNewCheckout is no longer just a boolean; it is specifically true.
However, this isn't enough yet. We need a way to derive types from this object so we can use them elsewhere in our application. This is where Mapped Types come into play. If you're interested in how similar techniques apply to other parts of your architecture, check out how I used TypeScript Mapped Types for Effortless API Integration Syncing to keep frontend models in sync.
Let’s say we want to create a FlagKey type that is automatically derived from our featureFlags object.
TYPESCRIPTtype FlagKey = keyof typeof featureFlags;
Now, if you try to pass a flag that doesn't exist to your lookup function, the compiler will scream at you. But we can take it a step further. What if we want to ensure that our application only handles flags that are explicitly defined as booleans?
We can use Mapped Types to create a specific interface for our configuration:
TYPESCRIPTtype BooleanFlags = { [K in keyof typeof featureFlags as(typeof featureFlags)[K] extends boolean ? K : never]: boolean; };
This filters our keys. Only the flags that are booleans are included in the BooleanFlags type. This is the same logic I often use when I discuss TypeScript Configuration Patterns: Enforcing Type-Safe Partial Defaults to prevent invalid state configurations.
When I first started refactoring our flag system, I tried to use an enum. It felt right, but it caused issues with tree-shaking and became a nightmare to maintain when we started pulling flag values from an external API.
The const assertion approach is superior because:
featureFlags constant, TypeScript will highlight every file in your project that needs an update.Sometimes, a flag isn't just a boolean. Maybe it's a numeric limit or a configuration string. If you need to enforce deep structural integrity for these, you might want to look into TypeScript Recursive Conditional Types for Safer Configuration Objects.
For most feature flags, however, keeping it simple is the best path. Use a union type for keys and a constrained object for the values.
Can I still fetch flags from an API?
Yes. You can define the shape of your API response using the types we derived. Use a type guard to ensure the incoming JSON matches your expected featureFlags structure at runtime.
Does this work with dynamic feature flag names? If the flag name is truly dynamic (e.g., coming from a user-inputted string), you will eventually hit a wall where TypeScript can't help you. Use a branded type to wrap those strings, as discussed in TypeScript Branded Types: Enforcing Domain Integrity at Compile-Time, to track where these "unsafe" strings enter your system.
What if I have hundreds of flags?
If you have a massive number of flags, I recommend splitting them into smaller, domain-specific objects rather than one giant configuration file. You can then compose these objects into a main AppFlags type.
I’m still experimenting with how to handle "stale" flags. While the types prevent errors, they don't automatically delete the keys from the code once they're no longer needed. I’m currently looking into custom ESLint rules to warn us when a flag has been marked as deprecated: true in our config object.
Don't over-engineer it. Start with as const, derive your FlagKey type, and watch how many runtime errors simply vanish from your logs.
TypeScript recursive conditional types help you prevent impossible states in complex configuration objects. Learn to enforce deep type safety at compile-time.