Skip to main content

The Type Safety Problem

Traditional feature flag systems use string literals and untyped values:
// ❌ Problems with traditional approach
const isEnabled = flags.get("new_checkout"); // Typo in flag name
const theme = flags.get("theme"); // What type is this?
const maxItems = flags.get("maxItems"); // Could be undefined

// Runtime errors waiting to happen!
if (theme.toUpperCase() === "DARK") {
  // TypeError if theme is undefined
  // ...
}

FlagKit’s Type-Safe Approach

FlagKit generates TypeScript types from your flag definitions:
// ✅ FlagKit with full type safety
import { flags } from "./.flagkit/generated/client";

// Autocomplete knows all available flags
const isEnabled = flags.get("newCheckout"); // boolean
//                          ^ Autocomplete suggests: newCheckout, theme, maxItems

// Type is enforced
const theme = flags.get("theme"); // 'light' | 'dark' | 'auto'

// Compiler catches typos
const broken = flags.get("them"); // ❌ Compile error: 'them' doesn't exist

Generated Types

Running flagkit generate creates comprehensive TypeScript types:
// All flag names as union type
export type FlagName = 
  | 'newCheckout'
  | 'theme'
  | 'maxItems'
  | 'apiEndpoint';

Type-Checked API

The flags.get() method is fully type-checked:
// Generic signature
function get<K extends FlagName>(name: K, context?: FlagContext): FlagValues[K];

// Usage examples
const a: boolean = flags.get("newCheckout"); // ✅ Correct type
const b: string = flags.get("newCheckout"); // ❌ Type error!

const c: "light" | "dark" | "auto" = flags.get("theme"); // ✅
const d: string = flags.get("theme"); // ❌ Type error - too broad!

// Type inference works
const theme = flags.get("theme");
if (theme === "dark") {
  // ✅ TypeScript knows theme is the enum
  applyDarkMode();
}

Compile-Time Validation

FlagKit catches errors at compile time, not runtime:

1. Typos in Flag Names

// ❌ Compile error
const value = flags.get("newChckout");
//                      ~~~~~~~~~~~~
// Error: Argument of type '"newChckout"' is not assignable
// to parameter of type 'FlagName'

2. Invalid Default Values

flags.config.ts
// ❌ Compile error
export const flags = defineFlags({
  theme: flag.enum(["light", "dark"]).default("auto"), // 'auto' not in enum!
  //       ~~~~~~
  // Error: Argument of type '"auto"' is not assignable to
  // parameter of type '"light" | "dark"'
});

3. Type Mismatches

// ❌ Compile error
const maxItems: string = flags.get("maxItems");
//    ~~~~~~~~
// Error: Type 'number' is not assignable to type 'string'

Refactoring Safety

Rename flags confidently using TypeScript’s refactoring tools:
// Before: Flag named 'newCheckout'
const isEnabled = flags.get("newCheckout");

// After: Rename to 'newCheckoutFlow'
// 1. Rename in flags.config.ts
// 2. Run `flagkit generate`
// 3. TypeScript shows all places that need updating

const isEnabled = flags.get("newCheckout");
//                          ~~~~~~~~~~~~
// Error: Argument of type '"newCheckout"' is not assignable...
Use your IDE’s “Find All References” feature to locate all usages of a flag before renaming.

Type-Safe Context

Context values are also type-checked:
// Define custom context type
export type AppContext = {
  userId: string; // Required
  plan: "free" | "pro";
  betaTester?: boolean;
};

// Use with flags
const isEnabled = flags.get("newCheckout", {
  userId: user.id,
  plan: user.plan,
  betaTester: user.isBeta,
  // unknownKey: 'value'  // ❌ Error if context is strictly typed
});

Enum Flags

Enum flags get full type safety:
flags.config.ts
export const flags = defineFlags({
  theme: flag.enum(["light", "dark", "auto"]).default("auto"),
});
// Usage is type-safe
const theme = flags.get("theme");

// TypeScript knows exact values
if (theme === "light") {
  // ✅
  setLightMode();
} else if (theme === "dark") {
  // ✅
  setDarkMode();
} else {
  setAutoMode(); // theme is 'auto'
}

// Invalid values caught
if (theme === "blue") {
  // ❌ Compile error
  //          ~~~~~~
  // Error: This condition will always return 'false'
}

Number Flags with Constraints

Number flags can have min/max constraints:
flags.config.ts
export const flags = defineFlags({
  pageSize: flag.number().min(1).max(100).default(20),
});
// Runtime validation
const pageSize = flags.get("pageSize"); // number (1-100)

// TypeScript can't enforce numeric ranges, but runtime will
// If someone edits decision-tree.json to set pageSize: 200,
// FlagKit will clamp to 100
TypeScript can enforce type (string, number, boolean) but not value ranges. Runtime validation ensures values stay within constraints.

Pattern-Based String Flags

String flags can use regex patterns for validation:
flags.config.ts
export const flags = defineFlags({
  apiEndpoint: flag
    .string()
    .pattern(/^https:\/\//)
    .default("https://api.example.com"),

  emailDomain: flag
    .string()
    .pattern(/^[\w-]+(\.[\w-]+)*\.\w+$/)
    .default("example.com"),
});

Benefits Summary

Catch Errors Early

Compile-time validation prevents runtime failures

IDE Support

Autocomplete, go-to-definition, and refactoring tools

Self-Documenting

Types serve as inline documentation

Refactor Safely

Rename flags without breaking code

Next Steps

Deterministic Rollouts

Learn about type-safe percentage-based rollouts