API reference
@evanion/react-feature 0.1.0 and its patch releases up to 0.1.1, the version on npm. Values on this page were produced by running 0.1.0 in CI at ec4c3e0 and are not re-executed.
@evanion/react-feature exports one provider, four hooks, a factory that binds them to a store, and six types of its own. It re-exports 33 @evanion/feature types, names unchanged, from BaseFieldType down. The core’s values, such as createFeatures, parseFeatureConfig and serializeConfig, come from @evanion/feature and are documented in the core API reference.
Publishes one store’s decisions to every component under it.
Full documentation
With decisions left out the provider resolves features against
context, once per identity of the two. With a set supplied it publishes
that set: the server rendered HTML from those decisions and counted an
exposure for each variant it assigned, so a client answering differently
makes an experiment result wrong by an amount no analysis recovers. A
version mismatch renders the shipped set unless onVersionMismatch names
're-resolve'.
Decides nothing itself. @evanion/feature resolves, and this component
writes the answer into React context and renders children.
Every hook this package exports throws outside a provider, and the message
names the provider and the package. useFeature,
useFeatureEnabled and useVariant throw for a key the
published decisions do not carry.
To publish decisions to your component tree, you use the FeatureProvider. This provider takes a store and publishes decisions at the erased types; every hook below it reads Decision<FeatureKey> per key, regardless of the schema the store carried. For a form that keeps the schema, use createFeatureContext.
function < extends <keyof , VariantInfo | never>>({ , , , , , , }: <>): import("react")..;
What the tests state
28 behaviours
- FeatureProvider
- renders the shipped answer where its own store resolves another one
- throws for a key the shipped set does not name
- runs no diff in a runtime that defines no process global
- renders in a runtime that defines no process global
- renders in a runtime whose process carries no env
- emits no resolve event for an observed store with no observer installed
- renders through every check it runs with no observer installed
- keeps reporting after an observer throws
- names the missing context field under NODE_ENV production
- reports the two versions under NODE_ENV production
- renders what it resolves itself under re-resolve in NODE_ENV production
- runs no diff under re-resolve with an observer in NODE_ENV production
- reports once across a re-render that writes a new observer
- reports again when a re-render changes the context
- publishes the set a re-render hands it
- publishes what it resolves when a re-render changes the policy
- delivers to an observer installed after the mount
- delivers to the observer the last render installed
- renders the shipped decisions when the two versions disagree
- renders what it resolves itself under re-resolve
- resolves its own store when no set was shipped
- renders on through an onDivergence that throws
- runs the decision diff outside NODE_ENV production
- runs no decision diff under NODE_ENV production
- exposes the resolved decision for every feature
- cascades through the provider, explanation included
- resolves once per context rather than on every render
- accepts decisions resolved elsewhere, such as a build snapshot
Each line is the name of a test in the package. A test states a behaviour; whether it proves one is a question its own source answers, and the pane carries that source.
import { } from '@evanion/feature';
import { , } from '@evanion/react-feature';
import { } from 'react-dom/server';
const = ([{ : 'express-pickup', : true }]);
const = {
: 'cust-0042',
: new ('2026-11-01T00:00:00Z'),
};
function () {
if (!('express-pickup')) return <>Standard checkout</>;
return <>Checkout with express pickup</>;
}
const = (
< ={} ={}>
< />
</>,
);
; // -> '<p>Checkout with express pickup</p>'Binds a store’s schema to a provider and a set of hooks.
Full documentation
createFeatures([...]) returns a Features<S> whose keys and variant types the
definitions supplied, and the store loses all of it the moment the value
reaches a context: createContext fixes its type when the context
is made, and useContext hands a hook that fixed type whatever the provider
above it was given. A function that makes the context and the hooks together
is where S can be held, so this is a factory and not a generic provider.
S comes from the argument. Nothing is restated at the call site, and a
store whose configuration arrived as JSON yields the wide hooks, which are
the ones useFeature and its siblings already export.
Each call makes its own context, so two stores nest and each set of hooks
reads its own provider. The returned provider also publishes to the shared
context, so a component calling the package’s own useFeature
underneath it reads the same decisions.
A browser decides what a reader sees and gates nothing. A key that type-checks here says the store this was built from declares it, and says nothing about the decisions the provider was handed at runtime.
To keep your store’s schema and allow for nested stores, you can use createFeatureContext. Each call makes its own React context, so two stores can nest and each set of hooks will read its own provider. The returned provider also publishes to the shared context, meaning a component calling this package’s own useFeature underneath it reads the same decisions.
function < extends <keyof , VariantInfo | never>, extends boolean = boolean>(: <, >): <, >;
What the tests state
11 behaviours
- createFeatureContext
- publishes the shipped set through the bound provider
- reports the two versions through the bound provider
- compares the substituted store version and not the factory store one
- renders the shipped set through the bound provider on a mismatch
- renders what the bound provider resolves itself under re-resolve
- renders the bound provider and answers its own hooks
- publishes to the package context, so the plain hooks read the same decisions
- resolves a store handed to the provider over the one the factory holds
- keeps each factory reading its own store when two nest
- reads a feature keyed by a numeric enum
- throws for a key the bound provider does not carry
Each line is the name of a test in the package. A test states a behaviour; whether it proves one is a question its own source answers, and the pane carries that source.
import { } from '@evanion/feature';
import { } from '@evanion/react-feature';
import { } from 'react-dom/server';
const = ([
{
: 'checkout-cta',
: true,
: [
{ : 'control', : 50 },
{ : 'blue', : 50, : { : 'Get it' } },
],
},
]);
const { , } = ();
function () {
const { } = ('checkout-cta');
return < ="button">{?. ?? 'Buy'}</>;
}
function () {
// @ts-expect-error -- 'checkout-button' is not a key of this store
return <>{('checkout-button').}</>;
}
const = (: string) =>
(
< ={{ }}>
< />
</>,
);
('cust-0199'); // -> '<button type="button">Get it</button>'
('cust-0042'); // -> '<button type="button">Buy</button>'One feature’s decision, explanation included.
Full documentation
Throws for a key that is not configured. createFeatureContext binds
this hook to one store’s keys, so a component under a bound provider names a
key the compiler checks.
To read a decision for a specific flag, use the useFeature hook. If you call it for a key the provider does not publish, it throws an error naming the key and the provider. The decision’s reason, rule and assignment are what the core’s Decisions page describes.
function (: ): <>;
What the tests state
1 behaviour
- useFeature
- throws for a feature that is not configured
Each line is the name of a test in the package. A test states a behaviour; whether it proves one is a question its own source answers, and the pane carries that source.
const = ([
{ : 'express-pickup', : true },
{ : 'gift-cards', : true },
]);
function () {
return <>{('express-pickup') ? 'Express' : 'Standard'}</>;
}
function () {
const { } = ('express-pickup');
const { } = ('express-pickup');
const = .(()).();
return (
<>{`${}/${ ?? 'no variant'}: ${.(', ')}`}</>
);
}
const = .();
.['express-pickup'] = {
: 'express-pickup',
: false,
: 'explicitly-off',
};
// A set shipped as JSON reaches the provider unchecked.
const = .(
.({
...,
: { 'gift-cards': .['gift-cards'] },
}),
);
const = (
< ={} ={}>
< />
</>
);
const = (
< ={} ={}>
< />
</>
);
const = (
< ={} ={}>
< />
</>
);
(); // -> '<p>Standard</p>'
(); // -> 'Error: feature "express-pickup" is not configured in this <FeatureProvider>'
(); // -> '<p>explicitly-off/no variant: express-pickup, gift-cards</p>'Every resolved decision, at the erased types.
When a component needs to check several flags at once, you use the useFeatures hook rather than calling useFeature per key. This hook returns every decision the provider published, organized by key.
function (): ;
What the tests state
1 behaviour
- useFeatures
- returns every decision at once
Each line is the name of a test in the package. A test states a behaviour; whether it proves one is a question its own source answers, and the pane carries that source.
const = ([
{ : 'express-pickup', : true },
{ : 'gift-cards', : true },
]);
function () {
return <>{('express-pickup') ? 'Express' : 'Standard'}</>;
}
function () {
const { } = ('express-pickup');
const { } = ('express-pickup');
const = .(()).();
return (
<>{`${}/${ ?? 'no variant'}: ${.(', ')}`}</>
);
}
const = .();
.['express-pickup'] = {
: 'express-pickup',
: false,
: 'explicitly-off',
};
// A set shipped as JSON reaches the provider unchecked.
const = .(
.({
...,
: { 'gift-cards': .['gift-cards'] },
}),
);
const = (
< ={} ={}>
< />
</>
);
const = (
< ={} ={}>
< />
</>
);
const = (
< ={} ={}>
< />
</>
);
(); // -> '<p>Standard</p>'
(); // -> 'Error: feature "express-pickup" is not configured in this <FeatureProvider>'
(); // -> '<p>explicitly-off/no variant: express-pickup, gift-cards</p>'One feature’s decision as a boolean.
For a component that simply shows or hides based on a flag, use the useFeatureEnabled hook. This is a convenience wrapper for useFeature(key).enabled.
function (: ): boolean;
What the tests state
1 behaviour
- useFeatureEnabled
- returns the boolean decision
Each line is the name of a test in the package. A test states a behaviour; whether it proves one is a question its own source answers, and the pane carries that source.
import { } from '@evanion/feature';
import { , } from '@evanion/react-feature';
import { } from 'react-dom/server';
const = ([{ : 'express-pickup', : true }]);
const = {
: 'cust-0042',
: new ('2026-11-01T00:00:00Z'),
};
function () {
if (!('express-pickup')) return <>Standard checkout</>;
return <>Checkout with express pickup</>;
}
const = (
< ={} ={}>
< />
</>,
);
; // -> '<p>Checkout with express pickup</p>'One feature’s assigned variant and its value.
Full documentation
A reader over the decision useFeature already returns. Both are absent for
a feature that resolved off and for a feature declaring no variants, so
calling code handles off before it switches on a name.
Throws for a key the provider does not carry, which useFeature does for the
same reason: a silent undefined would make a typo indistinguishable from a
feature nobody assigned.
To determine which variant of a feature is active, use the useVariant hook. Both fields are absent for a feature that resolved off and for a feature declaring no variants, so calling code should handle the “off” state before switching on a name.
function (: ): <, 'variant' | 'value'>;
What the tests state
3 behaviours
- useVariant
- reads the assigned variant and its value
- reads no variant from a feature that resolved off
- throws for a key the provider does not carry
Each line is the name of a test in the package. A test states a behaviour; whether it proves one is a question its own source answers, and the pane carries that source.
const = ([
{ : 'express-pickup', : true },
{ : 'gift-cards', : true },
]);
function () {
return <>{('express-pickup') ? 'Express' : 'Standard'}</>;
}
function () {
const { } = ('express-pickup');
const { } = ('express-pickup');
const = .(()).();
return (
<>{`${}/${ ?? 'no variant'}: ${.(', ')}`}</>
);
}
const = .();
.['express-pickup'] = {
: 'express-pickup',
: false,
: 'explicitly-off',
};
// A set shipped as JSON reaches the provider unchecked.
const = .(
.({
...,
: { 'gift-cards': .['gift-cards'] },
}),
);
const = (
< ={} ={}>
< />
</>
);
const = (
< ={} ={}>
< />
</>
);
const = (
< ={} ={}>
< />
</>
);
(); // -> '<p>Standard</p>'
(); // -> 'Error: feature "express-pickup" is not configured in this <FeatureProvider>'
(); // -> '<p>explicitly-off/no variant: express-pickup, gift-cards</p>'What a provider publishes, at the erased types.
Full documentation
Every Decision<'cta', 'control' | 'blue', { label: string }> assigns to
Decision<FeatureKey> because key, variant, value, blockedBy and
cause all sit in output position. A React context fixes its type when the
context is created and cannot hold the schema a provider was given, so a
provider widens to this once and the bound hooks narrow back.
Decision<FeatureKey> and not Decision: the parameter defaults to string
while its constraint admits a number, so a store keyed on a numeric enum
produces a Decision<7> whose key does not assign to Decision’s. The
widening only holds at the constraint.
The AnyDecisions type represents what FeatureProvider publishes and what useFeatures answers. Because a React context fixes its type when created and cannot hold a schema the compiler has not resolved, a provider widens to this type once and the bound hooks narrow back.
export type = <<, <>>>;What FeatureProvider takes.
Full documentation
features is required, because this provider holds no store of its own.
createFeatureContext is the form that already holds one, and
BoundFeatureProviderProps is what its provider takes.
S is the store’s schema, and it types decisions and onDivergence on
the keys that store declares, so a set another store resolved is a compile
error at the mount. The hooks underneath read AnyDecisions whatever
S is, which is the erasure a React context imposes and the reason the
factory exists.
The FeatureProviderProps type defines the requirements for the provider. The features prop is required here, whereas it is optional on BoundFeatureProviderProps. The context prop is memoised on identity, and decisions takes the set Features.snapshot produced elsewhere.
interface < extends <keyof , VariantInfo | never> = > {
: <>;
?: EvaluationContext;
?: <>;
?: <keyof & >;
?: ;
?: ;
}
Props of the provider createFeatureContext returns.
Full documentation
features is optional here and required on FeatureProvider: the
factory already holds a store, and a mount that supplies the context alone is
the common one. Pass it to resolve a different store with the same schema —
a per-tenant configuration, or the copy a browser rebuilt from JSON.
Frozen comes from the factory’s own store, and a substituted store carries
the same parameter. The hooks type their decisions off the factory’s answer,
so a store that freezes what it resolves under a factory built from a store
that freezes nothing would publish frozen decisions to hooks promising the
mutable form. The parameter here turns that mount into a compile error.
The BoundFeatureProviderProps type is what the provider returned by createFeatureContext takes. It includes Frozen from the factory’s own store; this turns a mount that would publish frozen decisions to hooks promising the mutable form into a compile error.
interface < extends <keyof , VariantInfo | never>, extends boolean = boolean> {
?: <, >;
?: EvaluationContext;
?: <, >;
?: <keyof & >;
?: ;
?: ;
}
What FeatureContext.useVariant answers for one key.
Full documentation
The [S[K]] extends [never] guard is what keeps a variant-free feature
honest. never is a subtype of every type, so the infer branch alone would
read variant off a decision that dropped the property and infer unknown.
The tuple wrap suppresses distribution and asks the question the branch
means to ask. Both fields come off Decisions<S>[K], so this and the
decision for the same key cannot drift apart.
The BoundVariant type is what useVariant answers for one key when using FeatureContext. Both fields come off Decisions<S>[K], ensuring this and the decision for the same key cannot drift apart.
export type < extends <keyof , VariantInfo | never>, extends keyof > = [
[]
] extends [
never
] ? {
?: undefined;
?: undefined;
} : <>[] extends {
?: infer ;
?: infer ;
} ? {
?: ;
?: ;
} : never;A provider and hooks bound to one store’s schema.
Full documentation
Each hook names keyof S, so a misspelled key is a compile error and the
variant a reader switches on is the union that store declares.
Frozen carries the store’s own answer. A store built with no observer
freezes nothing and its hooks answer the mutable decisions, a store built
with one answers the deeply readonly form, and a store whose observation the
compiler cannot settle answers the union of the two.
The FeatureContext type represents the provider and four hooks that createFeatureContext returns. Each hook names keyof S, meaning a misspelled key is a compile error and the variant a reader switches on is the union that store declares.
interface < extends <keyof , VariantInfo | never>, extends boolean = boolean> {
(: <, >): ;
(): <, <>>;
< extends keyof & >(: ): <, <>[]>;
< extends keyof & >(: ): boolean;
< extends keyof & >(: ): <, >;
}
What a consumer does when the shipped version is not the store’s.
The VersionMismatchPolicy determines what a provider renders when the shipped set states one config version and the store states another. The default is 'use-shipped', while 're-resolve' is intended for applications that run no experiment and care about revocation latency. An 'unversioned' outcome never re-resolves, because one of the two sides carrying no version proves nothing about the other.
export type = 'use-shipped' | 're-resolve';The base of a declared context field type.
Full documentation
Ported from libs/acl/src/types.ts:157-171. instant is a point in time
carried as an ISO 8601 string or epoch milliseconds, which is what toEpoch
at conditions.ts:18-22 already accepts.
To categorize the data allowed in a context field, you use BaseFieldType. This type defines the four fundamental types a declared context field may hold: 'string', 'number', 'boolean', and 'instant'.
This serves as the foundation for FieldType. In practice, a React application that defines its own document schema will name this type there, and Distribution is where that schema is described.
export type = 'string' | 'number' | 'boolean' | 'instant';The envelope without its payload, which a serializer writes around a store.
Full documentation
digest is fenced to never rather than omitted. Omitting it drops the
member from the type and still admits a whole FeatureConfig, because
TypeScript refuses an extra property on a fresh object literal and nowhere
else. A document’s digest is always configDigest of that same document,
so a serializer that copied one from its caller would emit a document whose
digest covers other bytes. Every holder recomputes it,
disagrees, reports digest-mismatch and refuses the whole document, and no
holder recovers on its own. configDigest is the one writer of the member.
When you need to handle a document without its associated payload, you use the ConfigEnvelope type. This is particularly useful for servers that serialize a store for the page they ship; the server will name this type during the call to serializeConfig on the core entry, which takes an envelope and writes it around a store.
Note that the digest member is fenced to never because configDigest is the sole writer of that member. For more details on the serializer, see the core API reference.
export type = <, 'features' | 'digest'> & {
readonly ?: never;
};One defect validateConfig found, with enough detail to name the row.
The ConfigIssue type describes what a failed validateConfig lists. A component that validates a document it fetched names this in the branch that reports the failure.
interface ConfigIssue {
: ;
: string;
?: ;
?: string;
}
What validateConfig found wrong. One code per class of defect.
To programmatically determine why a document was refused without relying on fragile message text, you can branch on a ConfigIssueCode. This type represents the code found on a ConfigIssue, with one unique code assigned per class of defect.
These codes are produced by validateConfig, which is documented in the core API reference.
export type = 'duplicate-feature' | 'unknown-dependency' | 'cycle' | 'duplicate-variant' | 'unknown-variant' | 'invalid-weight' | 'empty-variants' | 'zero-weights' | 'duplicate-rule-id' | 'duplicate-variant-order' | 'invalid-variant-order' | 'invalid-instant' | 'unknown-context-field' | 'field-type-mismatch' | 'unfenced-schema' | 'missing-schema-version' | 'unknown-member' | 'digest-mismatch';The context fields rules read.
A document describes its requirements through a schema. The ContextSchema type represents the half of the FeatureSchema dedicated to declaring the rules for context fields, with each field assigned a FieldType.
The two vocabularies stated by a document are further described in Distribution.
interface ContextSchema {
readonly ?: <<string, >>;
}
The canonical configuration document: an envelope over a list of definitions.
Full documentation
There is no bare-array form, for the reason Matrix at
libs/acl/src/types.ts:220-234 gives: a foreign producer emitting JSON
states the version and the schema, and one document crosses a boundary
without a wrapper assembled at the call site.
F defaults to FeatureKey. An entry point that reads a whole document
names the bare FeatureConfig, and a document keyed on a numeric enum
reaches that parameter only while the default admits a number.
The FeatureConfig type is the served document a store is built from. A React application that reloads its store from a poller writes the handler beside the component, and that file names this type. Distribution is where a document is served and reloaded.
interface < extends = > {
readonly ?: string | number;
readonly ?: string;
readonly ?: FeatureSchema;
readonly ?: string;
readonly ?: number;
readonly : readonly <>[];
}
The typing contract a document states.
Full documentation
Two vocabularies, because the two halves describe different things. For a
context field, validateConfig checks that context.fields declares the
field and that the condition’s operator fits the declared type’s []
suffix. It compares no condition value with the declared base name, so a
condition over a field declared number carrying a string passes. The
engine reads no variant value at all.
The FeatureSchema type acts as the typing contract that a document states, carried at the schema property. It manages two distinct vocabularies:
ContextSchema: For fields the engine reads and compares.FeatureShape: One per feature, for variant values the engine never reads.
The division between these two vocabularies is described in Distribution.
interface FeatureSchema {
readonly ?: ContextSchema;
readonly ?: <<string, FeatureShape>>;
}
What a schema records about one feature.
To record the specifics of a single feature within a schema, you use the FeatureShape type. It maps feature variants by name, with each mapping to a ValueShape. Because the document itself maintains the order that a rollout follows, this map carries no inherent order.
interface FeatureShape {
readonly ?: <<string, >>;
}
One context field’s declared type.
Full documentation
A flat string, so the whole schema is JSON a producer in any language emits
by reflection. [] is an array of the base type, and a trailing ? marks a
field a complete context may leave out.
The FieldType type defines the declared type of a single context field. To ensure a producer in any language can emit the entire schema via reflection, this is written as a flat string.
You can modify the base type using these markers:
[]: Indicates an array of the base type.?: A trailing question mark marks a field that a complete context may leave out.
export type = | `${}[]` | `${}?` | `${}[]?`;The JSON a condition value may hold.
Full documentation
AttributeCondition.value is unknown, because a store built from a literal
holds whatever an author wrote. One of those values reaches the next process
as something else while canonical erases the difference: a Date, which
canonical writes as the ISO string JSON.parse hands back. The publisher
and the holder then digest alike over two values evaluateCondition compares
with ===, and § 2 reads two agreeing digests as a proof that two processes
hold one configuration. This type refuses a Date wherever it appears.
An object member written as undefined is legal, which § 8 states:
canonical.ts:46 filters the member out and JSON.stringify omits it, so an
absent key and a key written as undefined canonicalize to one text and
digest alike over bytes that agree. The member type admits undefined so
that { tier } read off a nullable column and { tier?: string } off a
generated type both reach this type, since canonical writes the two as the
same text. TypeScript strips undefined from an optional member when it
checks one against an index signature, so a fence here would admit the second
spelling and refuse the first.
An array element stays fenced to JsonValue. canonical writes an
undefined element as the text undefined and JSON.stringify writes it as
null, so the publisher and the holder compute two digests over one array
and the holder reports digest-mismatch.
The object arm is an index signature. TypeScript derives one implicitly for a
type alias and for an object literal, optional members included, and never
for an interface, so a value annotated with an interface does not reach
this type. § 4 declares every context field as a flat FieldType, so no
generator emits an interface for a condition value. A generated type lands at
SerializedVariantSpec.value, which carries unknown for that reason.
For the values held by a condition, the system uses the JsonValue type, which SerializedAttributeCondition uses to narrow its value property.
Crucially, JsonValue refuses Date objects wherever they appear. This is because a Date reaches the next process as an ISO string, and canonical writes the two the same way: the publisher and the holder then digest alike, which a reader takes as proof that the two hold one configuration, while evaluateCondition compares the two values with === and finds them unequal.
export type = string | number | boolean | null | readonly [] | {
readonly [: string]: | undefined;
};What a reload did.
Full documentation
Both arms carry version, and on both arms it names the document that is
installed now. A refusal keeps the previous document, so its version is the
one that stayed and rejected is the one that did not arrive.
The ReloadResult type is what features.reload answered. The poller’s handler names it, and its changed list is keyed on FeatureKey.
export type = {
: true;
: string | number | undefined;
: string | number | undefined;
: readonly [];
} | {
: false;
: string | number | undefined;
: string | number | undefined;
: readonly ConfigIssue[];
};An attribute condition as a document carries it, compared with ===.
To evaluate a feature based on custom data, the system uses attribute conditions. When these conditions are carried within a document, they are represented as SerializedAttributeCondition, where the value is narrowed to JsonValue. The engine performs a strict equality check (===) when comparing this value.
interface SerializedAttributeCondition extends <AttributeCondition, 'value'> {
readonly : ;
}
A condition as a document carries it. The instant and the value narrow.
A feature’s activation often depends on a variety of triggers, such as time windows, days of the week, or specific attributes. SerializedCondition represents a single condition as it is carried in a document across these different arms. The instant a window names and the value an attribute condition compares are the two members that narrow against the form held by a store built from a literal.
export type = SerializedWindowCondition | DayOfWeekCondition | SerializedAttributeCondition;A definition as a document carries it.
Full documentation
F defaults to FeatureKey, the default FeatureConfig carries. This type
is the element type of FeatureConfig['features'], so a helper annotated
with the bare form reads a definition out of a bare document, and a
definition keyed on a numeric enum reaches both names.
A document carries its feature configurations in a features array, where each element is a SerializedDefinition. Because its key parameter defaults to FeatureKey, the same default carried by a document, a helper annotated with the bare name can read a definition directly out of a bare document.
interface < extends = > extends <<>, 'rules' | 'variants'> {
readonly ?: readonly SerializedRule[];
readonly ?: readonly SerializedVariantSpec[];
}
An Instant that survives JSON: an ISO 8601 string, or epoch milliseconds.
Full documentation
Instant at types.ts:16 admits a Date, which a literal in a TypeScript
file is what an author writes. JSON.stringify writes that Date as an ISO
string and JSON.parse hands back a string, so a document that made one hop
through a transport holds a different value than the one that authored it.
The two evaluate alike, because toEpoch in conditions.ts reads both to
one number, and configDigest over the two disagrees. A document narrows to
this, and serializeConfig converts on the way out.
While the core Instant type admits a Date (which is what an author uses in a literal), data passing through JSON must be represented differently. SerializedInstant is an instant that survives JSON transport, appearing as either an ISO 8601 string or epoch milliseconds. A document narrows to this type so that a document making one hop through a transport is digested the same way as the one that authored it.
export type = string | number;A rule as a document carries it, with its control-plane id untouched.
Rules define the logic for feature assignment. When a rule is carried in a document, it is typed as SerializedRule, which keeps the control-plane id untouched but narrows the when property to an array of SerializedCondition. These rules are written within Rollouts.
interface SerializedRule extends <Rule, 'when'> {
readonly ?: readonly [];
}
A variant as a document carries it.
Full documentation
value stays unknown. § 4 picks JSON Schema for this member because
quicktype and openapi-generator emit TypeScript interfaces from it, and
TypeScript gives an interface no implicit index signature, so a JsonValue
here refuses the generated type the notation was picked to produce. § 8 puts
the round trip in serializeConfig, which converts a Date wherever it
appears, and § 7’s validator checks a variant value against the shape its
schema declares.
Each possible outcome of a feature is a variant. SerializedVariantSpec represents one such variant as it is carried in a document. The value remains unknown because the engine never reads a variant value, and a generated TypeScript interface does not reach JsonValue.
interface SerializedVariantSpec extends <VariantSpec, 'value'> {
readonly ?: unknown;
}
A window condition as a document carries it.
To restrict a feature to a specific timeframe, the system uses time windows. As carried in a document, a SerializedWindowCondition defines this window by setting the field to 'now', the op (operator) to either 'before' or 'after', and the value to a SerializedInstant.
interface SerializedWindowCondition {
: 'now';
: 'before' | 'after';
: ;
}
What validateConfig decided. The refusal carries every issue it found.
The ValidationResult type is what validateConfig answered about a document, as one of two arms.
export type = {
: true;
} | {
: false;
: readonly ConfigIssue[];
};A JSON Schema 2020-12 object, fenced to the subset validateConfig accepts.
Full documentation
The engine never reads a variant value: valueOf hands it to the caller
untouched. So this shape is what a code generator and an optional validator
read, and the notation is JSON Schema because quicktype and
openapi-generator already emit TypeScript, Swift and Kotlin from it.
To ensure variant values adhere to a specific structure, FeatureShape carries a ValueShape for each variant. This type is a JSON Schema 2020-12 object, fenced to the subset that validateConfig accepts. While a code generator and an optional validator read this shape, the engine hands the variant value to the caller untouched.
export type = <<string, unknown>>;Where a decision set was produced.
The DecisionOrigin type describes which kind of process resolved a shipped set. A provider handed a set from a per-request render adopts that set’s own instant, and a set from a build leaves the client’s clock to answer.
export type = 'render' | 'build';One subject’s answers for one instant, with the configuration that produced them.
Full documentation
It crosses no service boundary. The process that produced it and the process
that consumes it are two halves of one page load, which is the line § 8 of
docs/specs/2026-09-16-published-policy-contracts.md draws between a
document and a decision.
The DecisionSet type is what decisions takes on both providers. The set states the config version it was resolved under and the instant as an ISO 8601 string, which is what lets a provider tell whether the two processes hold one configuration.
interface < extends <keyof , VariantInfo | never> = , extends boolean = boolean> {
readonly ?: string | number;
readonly : string;
readonly : ;
readonly : <, <>>;
}
What a caller installs to hear about divergence.
The DivergenceObserver type is what onDivergence takes on both providers. It is notified for every divergence a provider detects and alters no decision.
export type < extends = string> = (: <>) => void;What a process could not reproduce, and why. Output only.
Full documentation
Nothing in the library reads a report back, which is the invariant
reason-is-output-only.spec.ts already holds for reason. A report names
the feature at key when one feature is at fault, and the context field at
field on a 'missing-field' report.
The DivergenceReport type represents one report an observer receives. A provider sends one per pass of its checks, and React invokes a render twice under StrictMode, so an observer that counts sees each report of that pass twice.
interface < extends = string> {
: 'config-version' | 'unversioned' | 'missing-field' | 'decision-differs';
?: ;
?: string;
?: DivergenceSide;
?: DivergenceSide;
: string;
}
One side of a comparison, at the members a report names.
When comparing decisions made by different environments, the system identifies which side of the comparison is being referenced. DivergenceSide represents one of these sides via four members. Because a DivergenceReport carries both shipped and local sides, an observer can read what the server decided beside what the local process decided.
interface DivergenceSide {
?: string | number;
?: boolean;
?: string;
?: 'weighted' | 'pinned' | 'sticky' | 'fallback';
}
One feature’s decision.
Full documentation
enabled is the decision. Everything else is explanation, and is output
only — nothing in this library reads reason, rule, rules, blockedBy,
cause, variant, value or assignment back to decide anything.
To handle the outcome of a feature check, the library uses the Decision type. This represents the specific decision made for a single feature.
When you use useFeature, the hook returns the erased form of this type, represented as Decision<FeatureKey>. Similarly, AnyDecisions acts as a container that holds one of these decisions per key.
It is important to note the distinction between the bare Decision type and Decision<FeatureKey>. Because the first parameter defaults to string but its constraint allows for a number, these are treated as two distinct types. If you use the bare name as a type annotation, it will refuse the value returned by the hook.
interface < extends = string, extends string = string, = unknown> {
: ;
: boolean;
: ;
?: string;
?: readonly RuleOutcome[];
?: ;
?: <>;
?: ;
?: ;
?: {
: 'weighted' | 'pinned' | 'sticky' | 'fallback';
: string;
?: number;
?: string;
};
}
One decision per feature, each narrowed to what its own variants allow.
Full documentation
The constraint names keyof S. An index signature in that position rejects
the schema a consumer writes as an interface, which is the one case the
hand-written schema exists for.
The Decisions type is what FeatureContext’s hooks answer at the factory’s schema, and BoundVariant reads variant and value off it.
export type < extends <keyof , VariantInfo | never> = > = {
[ in keyof ]: <, []>;
};Evaluation context. now is injected rather than read from an ambient clock,
defaulting to new Date() at the call.
The EvaluationContext type is what context takes on both providers. It is an argument the store is read against and has nothing to do with React context.
interface EvaluationContext {
?: ;
?: string;
?: <<string, string>>;
[: string]: unknown;
}
A feature identifier.
Full documentation
string | number rather than string, so a consumer can key features on a
numeric enum and still get exhaustiveness from Decisions.
The FeatureKey type is used as every hook’s key parameter, and the key type AnyDecisions is keyed on. It is string | number, so a store keyed on a numeric enum types its hooks on numbers.
export type = string | number;The store. It holds intent — enabled, dependencies and rules — and nothing
evaluated.
Full documentation
The only writers are toggle and editing the configuration. resolve and
plan are pure functions of (config, context, now). Writing a resolved
value back would put entries in an audit log that nobody performed, make the
store disagree between a process that slept through a window boundary and one
that did not, and destroy the distinction between “someone turned this off”
and “the system turned it off” — the one an operator needs at 3am.
Read this type off createFeatures, and name it in an annotation only where
a declaration demands one. const store = createFeatures([...]) carries a
settled Frozen, and resolve then answers one form. The annotation const store: Features<Flags> = createFeatures([...]) leaves Frozen at its
boolean default, resolve answers the union of both forms, and that union
assigns to neither Decisions<Flags> nor DeepReadonly<Decisions<Flags>>,
because DeepReadonly maps the Date a rule outcome can carry to a
ReadonlyDate. A declaration that must name the type carries the second
parameter with it: Features<Flags, false> for a store built with no
observer, Features<Flags, true> for one built with an observer. A function
taking either store names both parameters and stays generic over Frozen.
The bare Features serves a declaration a framework demands, where the
store’s schema arrived at runtime.
The Features type is what features takes on both providers, and what createFeatureContext takes. One @evanion/feature in the dependency graph is what makes the store an application built assign to the prop.
interface < extends <keyof , VariantInfo | never> = , extends boolean = boolean> {
readonly : readonly (keyof & )[];
readonly : string | number | undefined;
readonly : ;
readonly : readonly <keyof & >[];
(: keyof & ): <keyof & > | undefined;
(: keyof & ): readonly (keyof & )[];
(?: EvaluationContext): <, <>>;
(: keyof & , ?: EvaluationContext): boolean;
< extends keyof >(: , ?: EvaluationContext): <>[] extends {
?: infer ;
} ? | undefined : undefined;
< extends keyof >(: , ?: EvaluationContext): <>[] extends {
?: infer ;
} ? | undefined : undefined;
(?: EvaluationContext): <, <>>;
(?: EvaluationContext, ?: SnapshotOptions): <, >;
(?: EvaluationContext): <, >;
(: keyof & , : boolean, ?: EvaluationContext): <, <keyof & >>;
(: <keyof & >): ;
}
The deeply readonly form of T for a store that freezes what it answers, and
T itself for a store that freezes nothing.
Full documentation
Frozen is naked, so a store whose observation the compiler cannot settle
distributes to the union of both forms and a reader narrows it.
The FrozenWhenObserved type defines how FeatureContext’s useFeatures and useFeature type their answer. A store built with an observer freezes what it resolves and its hooks answer the deeply readonly form; a store built without one answers the mutable form.
export type < extends boolean, > = extends true ? <> : ;A map from feature key to what that feature’s variants are.
Full documentation
A consumer writes one of these to type a store built from configuration that
arrived as JSON, where there are no literals for InferSchema to read. A
store built from a literal gets its schema inferred and nobody writes this.
This alias annotates a value. A generic that takes a schema constrains it
self-referentially, S extends Record<keyof S, VariantInfo | never>, the way
Decisions and Plan do: the index signature this alias carries rejects the
interface a consumer writes by hand.
The Schema type is the default schema parameter given by FeatureProviderProps, meaning FeatureProviderProps needs no type argument.
export type = <string, VariantInfo | never>;What a schema records about one feature: its variant names and their values.
The VariantInfo type is the constraint every schema parameter in this package is written against: S extends Record<keyof S, VariantInfo | never>, where never is the entry a variant-free feature carries.
interface VariantInfo {
: string;
?: unknown;
}
Where to go next
- Getting started: the provider, the memoised context and the bound hooks, in the order you write them
- Core API reference: every type react-feature re-exports, and the functions a server calls
- Decisions: what a decision carries and what each
reasonmeans