Feature Flag Type System in TypeScript
Feature flags are a powerful technique for remotely controlling the behavior of your application. They allow you to enable or disable features dynamically without deploying new code. This challenge focuses on creating a robust and type-safe system for defining and managing different types of feature flags in TypeScript.
Problem Description
Your task is to design and implement a TypeScript type system that can represent various kinds of feature flags. This system should be flexible enough to handle simple boolean flags, flags with string values, and flags that return objects. The goal is to ensure type safety, so that when you check a flag's status or retrieve its value, TypeScript can provide compile-time guarantees about the expected type.
Key Requirements:
- Boolean Flags: A basic feature flag that is either enabled (true) or disabled (false).
- String Flags: A feature flag that, when enabled, returns a specific string value.
- Object Flags: A feature flag that, when enabled, returns an object with a predefined structure.
- Type Inference/Guards: The system should enable type inference so that when a flag is checked and determined to be enabled, its associated value type is correctly inferred.
- Unified Interface: Provide a consistent way to interact with all types of feature flags, regardless of their underlying type.
Expected Behavior:
- When a boolean flag is checked and enabled, it should evaluate to
true. - When a boolean flag is checked and disabled, it should evaluate to
false. - When a string flag is checked and enabled, it should return its associated string value.
- When a string flag is checked and disabled, it should return a default value (e.g.,
undefinedor a specific fallback string). - When an object flag is checked and enabled, it should return its associated object value.
- When an object flag is checked and disabled, it should return a default value (e.g.,
undefinedor an empty object). - The system should prevent accessing a string or object value from a flag that is not of that specific type.
Important Edge Cases:
- What happens when you try to access the value of a disabled flag?
- How does the type system handle the uncertainty of whether a flag is enabled or disabled at compile time?
Examples
Let's assume we have a hypothetical FeatureFlagRegistry that manages our flags.
Example 1: Boolean Flag
Consider a flag named "new-dashboard" that is either on or off.
// Assume a mechanism to define and retrieve flags
// For this example, we'll focus on the type definitions themselves.
// A simple boolean flag
type BooleanFlag = {
type: 'boolean';
defaultValue: boolean;
// Add any other relevant properties if needed
};
// In a real system, you'd have a registry like this:
// const registry = new FeatureFlagRegistry();
// registry.register('new-dashboard', { type: 'boolean', defaultValue: false });
// Usage in a type-safe manner:
// function displayDashboard(flags: { 'new-dashboard': BooleanFlag }) {
// const isNewDashboardEnabled = flags['new-dashboard']; // This should be a boolean
// if (isNewDashboardEnabled) {
// console.log("Showing the new dashboard!");
// } else {
// console.log("Showing the old dashboard.");
// }
// }
Output (Conceptual Type Inference):
When isNewDashboardEnabled is accessed, TypeScript should infer its type as boolean.
Explanation: The BooleanFlag type clearly indicates it's a boolean flag. When accessed, its resolved type should be boolean.
Example 2: String Flag
Consider a flag named "api-endpoint" that, when enabled, provides a specific API URL.
// A string flag
type StringFlag<T extends string = string> = {
type: 'string';
defaultValue: T | undefined; // Or a specific default string
};
// Usage in a type-safe manner:
// function callApi(flags: { 'api-endpoint': StringFlag<'/v1/users'> }) {
// const endpoint = flags['api-endpoint']; // This should be '/v1/users' | undefined
// if (endpoint) {
// console.log(`Calling API at: ${endpoint}`);
// } else {
// console.log("API endpoint not configured.");
// }
// }
Output (Conceptual Type Inference):
When endpoint is accessed from a flag of type StringFlag<'/v1/users'>, TypeScript should infer its type as '/v1/users' | undefined.
Explanation: The StringFlag type, when parameterized with a specific string literal (like '/v1/users'), ensures that the retrieved value, if enabled, will be of that exact string type or undefined if disabled.
Example 3: Object Flag
Consider a flag named "user-profile-settings" that, when enabled, returns an object with a specific configuration.
interface UserProfileConfig {
theme: 'dark' | 'light';
notifications: boolean;
}
// An object flag
type ObjectFlag<T> = {
type: 'object';
defaultValue: T | undefined; // Or a specific default object
};
// Usage in a type-safe manner:
// function renderUserProfile(flags: { 'user-profile-settings': ObjectFlag<UserProfileConfig> }) {
// const config = flags['user-profile-settings']; // This should be UserProfileConfig | undefined
// if (config) {
// console.log(`User profile theme: ${config.theme}`);
// console.log(`Notifications enabled: ${config.notifications}`);
// } else {
// console.log("Default user profile settings loaded.");
// }
// }
Output (Conceptual Type Inference):
When config is accessed from a flag of type ObjectFlag<UserProfileConfig>, TypeScript should infer its type as UserProfileConfig | undefined.
Explanation: The ObjectFlag type, when parameterized with an interface (like UserProfileConfig), ensures that the retrieved value, if enabled, will conform to that interface or be undefined if disabled.
Constraints
- The solution must be written in TypeScript.
- The type definitions should be as generic as possible to accommodate future flag types.
- The design should prioritize compile-time type safety. Runtime type checking should be handled by the underlying feature flag implementation (which you don't need to implement for this challenge, focus on the types).
- Aim for a solution that is extensible to new feature flag types with minimal modifications to the core type system.
Notes
- Think about how to represent the "state" of a flag (enabled/disabled) in relation to its type.
- Consider using discriminated unions or conditional types to manage the different flag types and their associated values.
- The examples show how flags might be used, but your primary focus is on defining the types themselves, and demonstrating how these types enable safe usage. You do not need to implement a full feature flag runtime or registry.
- Focus on creating the
typedefinitions and potentially a union type that encompasses all possible feature flag types.