Featurevisor

Tools

Code generation

Generate TypeScript from your definitions, so a misspelled feature key, a variable a feature does not have or a variation value that does not exist fails to compile.

Generate

From the project root:

Command
npx featurevisor generate-code \
--language typescript \
--out-dir ./src \
--react

TypeScript is the only language so far. --react adds typed hooks for React.

For checkout, pricing and buyButton from these docs, the generated Features type knows every variation value and variable type:

src/features.ts, excerpt
export type Features = {
buyButton: {
variation: "control" | "treatment";
label: string;
colour: string;
};
checkout: null;
pricing: {
variation: "control" | "treatment";
};
// ...
};

Global variables get the same treatment in variables.ts, where checkoutSettings becomes { currency: "EUR" | "GBP"; paymentMethods: string[] }.

What it writes

FileContains
features.tsFeature keys, variation values, and feature variable keys and types
variables.tsGlobal variable keys and types
context.tsA Context type built from your attributes
attributes.tsA type for each attribute, such as CountryAttribute
schemas.tsTypes for your schemas, if you have any
functions.tsTyped isEnabled, getVariation and getVariable
instance.tssetInstance and getInstance
react.tsTyped useFlag, useVariation and useVariable, with --react
index.tsEverything above

Context types accept what your application passes in, so a string with format: date-time becomes Date | string there. Variable types describe what the SDK returns, so the same format stays string in features.ts and variables.ts.

Relative imports end in .js, so the compiled files work in Node.js ES modules with TypeScript's NodeNext resolution, as well as with CommonJS and bundlers. Keep the extensions.

Generate for some datafiles

--tag and --target limit the code to what those tags and targets select. Both can be repeated, and everything any of them selects is included, so one package can serve the web and mobile datafiles:

Command
npx featurevisor generate-code \
--language typescript \
--out-dir ./src \
--target=web \
--target=mobile

A target selects as it does for a build. A target's context, which simplifies a datafile, does not remove any types, since generated code is not tied to one environment or one datafile.

Use it

Give the generated code your SDK instance once:

your-app/index.js
import { createFeaturevisor } from '@featurevisor/sdk'
import { setInstance } from '@yourorg/features'
const f = createFeaturevisor({
datafile: datafileContent,
})
setInstance(f)

Calling a generated function before setInstance() throws Call setInstance() before using generated functions.

The functions have the JavaScript SDK's names and arguments, with your keys and types:

import {
isEnabled,
getVariation,
getVariable,
} from '@yourorg/features'
const context = {
userId: '8f2a',
country: 'nl',
}
isEnabled('checkout', context)
getVariation('pricing', context)
getVariable('buyButton', 'label', context)
getVariable('supportEmail', context)

getVariation('pricing') returns 'control' | 'treatment' | null, and getVariable('buyButton', 'label') returns string | null. getVariable('checkout', 'label') does not compile, because checkout has no variables. A known attribute passed with the wrong type fails to compile too. Attributes the project does not define are still accepted.

React hooks

With --react, the hooks have the names of those in @featurevisor/react, and take the same arguments:

import {
useFlag,
useVariation,
useVariable,
} from '@yourorg/features'
const newCheckout = useFlag('checkout')
const variation = useVariation('pricing')
const label = useVariable('buyButton', 'label')
const supportEmail = useVariable('supportEmail')

Generated functions and hooks pass their types through the generic parameters of @featurevisor/sdk and @featurevisor/react, so a generated call and a hand typed one agree.

Clean up retired flags

Archived features are left out of the generated code. Archive or delete a feature, regenerate, and every call that still names it fails to compile, so a retired flag cannot leave dead code behind. See Retire it for the steps around it.

Share it as a package

Copy the generated code into one application, or publish it as a private package, such as @yourorg/features, for several. Publish it as an ES module, so bundlers can drop what an application does not import.

Generate and publish it in the same deployment that publishes the datafiles, so the types and the datafiles describe the same definitions.

Details

Composition types

A string alias inherits its global source's evaluated type, with conservative null support and the entire result recursively read only. It does not introduce an object property named after the source. JSON aliases use the decoded value type, not the stored string. Overrides preserve this inferred shape and do not narrow nullability.

For a map composition, TypeScript generation derives required properties from its map keys. Flag members are boolean. Variation members include their declared values and disabled fallback, with null where applicable. Variable members preserve the source schema type and permit null. Nested compositions stay nested and reusable schemas keep their imports. Source object and array types are recursively read only because getters can return borrowed values.

Sources are resolved from the complete project even when --tag or --target filters the generated roots. Do not author a schema or fake default on the composed definition to make generation work. Regenerate types after changing a composition or source schema, and compile your application against them. Generated types do not add composition support to an older SDK.

Map composition overrides do not widen the generated property set or replace the inferred schema. Whole replacements must contain exactly the properties and match the nullable source types. Mutations cannot remove required properties or add arbitrary ones. JSON sources are decoded at runtime and contribute an unconstrained member value type, not a string type. Literal dotted properties remain literal in generated types, but mutation path strings cannot address them as a single key.

Previous
Catalog