Type-safe feature flags for TypeScript applications
Gated is a type-safe feature flag library for TypeScript applications. It provides a flexible API for evaluating feature flags with full type inference, a hook system for extending behavior, and framework integrations. Works on both client-side and server-side environments.
Gated works by wrapping your existing feature flag provider (LaunchDarkly, PostHog, custom, etc.) with a type-safe interface. You provide the identity resolution and decision logic, and Gated handles the evaluation flow with hooks for caching, deduplication, logging, and other cross-cutting concerns.
- β Type-safe - Full TypeScript support with type inference for boolean and variant flags
- π Provider-agnostic - Works with any feature flag service (LaunchDarkly, PostHog, custom, etc.)
- πͺ Extensible hooks - Create custom lifecycle hooks for caching, logging, analytics, and more
- βοΈ Framework integrations - React hooks and components (React 19+ with
use()primitive) - π Universal - Works in both client-side and server-side environments
- π― Identity-based - Evaluate flags for specific users with custom identity types
- π¦ No runtime dependencies - React is an optional peer dependency
- π§ͺ Testable - Override identities and decisions for testing
npm install gatedimport { buildGate } from "gated";
// Create a gate factory with your provider's logic
const gate = buildGate({
identify: async () => ({ distinctId: getCurrentUserId() }),
decide: async (key, identity) => {
// Your provider's API call
const enabled = await yourProvider.isEnabled(key, identity.distinctId);
return { value: enabled };
},
});
// Create type-safe feature flags
const betaAccess = gate({ key: "beta-access", defaultValue: false });
const newDashboard = gate({ key: "new-dashboard", defaultValue: false });
// Evaluate flags
const hasBetaAccess = await betaAccess(); // false
const hasNewDashboard = await newDashboard(); // trueBoolean flags represent true/false feature toggles:
// Using the gate from Quick Start
const darkMode = gate({ key: "dark-mode", defaultValue: false });
if (await darkMode()) {
enableDarkMode();
}String variants for A/B tests or multi-option features:
const themeFlag = gate({
key: "theme",
defaultValue: "light", // Type-safe based on the variants array
variants: ["light", "dark", "system"],
});
const theme = await themeFlag(); // Type: "light" | "dark" | "system"Intercept the flag evaluation lifecycle with hooks. Gated supports five lifecycle stages:
before- Runs before flag evaluationresolve- Can short-circuit evaluation by returning a decisionafter- Runs after successful evaluationerror- Runs when evaluation throws an errorfinally- Always runs after evaluation completes
import { createHook } from "gated";
// Create a custom logging hook
const loggingHook = createHook(() => ({
before: async (context) => {
console.log(`Evaluating flag: ${context.flagKey}`);
},
after: async (context, decision) => {
console.log(`Result for ${context.flagKey}:`, decision);
},
error: async (context, error) => {
console.error(`Error evaluating ${context.flagKey}:`, error);
},
}));
// Add hooks when building your gate
const gate = buildGate({
identify: async () => ({ distinctId: userId }),
decide: async (key, identity) => provider.evaluate(key, identity),
hooks: [loggingHook()],
});Two hook implementations are included:
Caches flag decisions by identity:
import { cacheHook } from "gated/hooks";
const cache = {
get: async (key: string) => await redis.get(key),
set: async (key: string, value: Decision) => await redis.set(key, value),
};
// Add to your gate's hooks array
const gate = buildGate({
identify: async () => ({ distinctId: userId }),
decide: async (key, identity) => provider.evaluate(key, identity),
hooks: [cacheHook(cache)],
});Deduplicates concurrent requests for the same flag:
import { dedupeHook } from "gated/hooks";
// Add to your gate's hooks array
const gate = buildGate({
identify: async () => ({ distinctId: userId }),
decide: async (key, identity) => provider.evaluate(key, identity),
hooks: [dedupeHook()],
});
// Only one API call will be made even with concurrent evaluations
const [result1, result2] = await Promise.all([betaFlag(), betaFlag()]);React is currently the only framework with dedicated integration. The core library works in any JavaScript environment.
Convert gate functions into React hooks using the createReactHook() function. Components using these hooks must be wrapped in a Suspense boundary:
import { createReactHook } from "gated/react";
// Using the gate from Quick Start
const betaFlag = gate({ key: "beta-access", defaultValue: false });
export const useBetaAccess = createReactHook(betaFlag);
// Use in components (wrapped in Suspense)
function MyComponent() {
const hasBeta = useBetaAccess();
return hasBeta ? <BetaFeature /> : <OldFeature />;
}
function App() {
return (
<Suspense fallback={<Loading />}>
<MyComponent />
</Suspense>
);
}A convenience component for conditionally rendering children based on flag evaluation. Comes wrapped in a Suspense boundary to handle loading states:
import { FeatureGate } from "gated/react";
function App() {
return (
<FeatureGate
gate={useBetaAccess}
loading={<Spinner />}
fallback={<OldFeature />}
>
<BetaFeature />
</FeatureGate>
);
}For variant flags, use the match prop to specify the expected value (defaults to true for boolean flags).
Creates a gate factory function for evaluating feature flags.
const gate = buildGate({
identify: () => Promise<TIdentity>,
decide: (key: string, identity: TIdentity) => Promise<Decision>,
hooks?: Hook[]
})Returns a gate factory function that creates individual feature flags.
// Boolean flag
gate({ key: string, defaultValue: boolean }): () => Promise<boolean>
// Variant flag
gate({
key: string,
defaultValue: T,
variants: readonly T[]
}): () => Promise<T>Optionally accepts overrideIdentity parameter for testing.
Creates a reusable hook with typed options. See Hook System section for lifecycle methods.
const myHook = createHook((options: TOptions) => ({
before?: (context: HookContext) => void | Promise<void>,
resolve?: (context: HookContext) => Decision | undefined | Promise<Decision | undefined>,
after?: (context: HookContext, decision: Decision) => void | Promise<void>,
error?: (context: HookContext, error: unknown) => void | Promise<void>,
finally?: (context: HookContext) => void | Promise<void>
}))Converts a gate function into a React hook using React 19's use() primitive. Components using the hook must be wrapped in a Suspense boundary.
Conditionally renders children based on flag evaluation. Includes built-in Suspense boundary.
Props: gate, loading?, fallback?, overrideIdentity?, match?
Gated provides full type inference for variant flags and supports custom identity types:
// Custom identity types
interface UserIdentity extends Identity {
distinctId: string;
email: string;
plan: "free" | "pro" | "enterprise";
}
const gate = buildGate<UserIdentity>({
identify: async () => ({
distinctId: user.id,
email: user.email,
plan: user.plan,
}),
decide: async (key, identity) => {
// identity is fully typed as UserIdentity
return provider.evaluate(key, identity);
},
});Variant flag return types are automatically inferred from the variants array (see Variant Flags section).
Gated works with any feature flag provider by implementing the decide function:
import * as LaunchDarkly from "launchdarkly-js-client-sdk";
const ldClient = LaunchDarkly.initialize("client-id", { key: "user-key" });
const gate = buildGate({
identify: async () => ({ distinctId: getCurrentUserId() }),
decide: async (key) => ({ value: await ldClient.variation(key, false) }),
});import posthog from "posthog-js";
const gate = buildGate({
identify: async () => ({ distinctId: getCurrentUserId() }),
decide: async (key) => ({ value: posthog.isFeatureEnabled(key) }),
});const gate = buildGate({
identify: async () => ({
distinctId: user.id,
email: user.email,
country: user.country,
}),
decide: async (key, identity) => {
const res = await fetch(`/api/features/${key}`, {
method: "POST",
body: JSON.stringify(identity),
});
return { value: (await res.json()).enabled };
},
});Override identities when testing:
const betaFlag = gate({ key: "beta-access", defaultValue: false });
// Test with specific identity
const result = await betaFlag({ distinctId: "test-user-123" });
// Or use a test gate with mocked decide function
const testGate = buildGate({
identify: async () => ({ distinctId: "test" }),
decide: async (key) => ({ value: key === "beta-access" }),
});See CONTRIBUTING.md.