Skip to content

Custom Conditions ​

showwhat ships with a handful of built-in condition types, but the real power is in extending it with your own. The condition system is fully open — any object with a type string passes schema validation, and you provide the evaluator function that decides what it means.

How evaluation works ​

When the resolver encounters a condition, it looks up the evaluator in the evaluators map by condition.type:

  1. If the type is and or or, the resolver handles it internally (recursive composite evaluation)
  2. Otherwise, it calls evaluators[condition.type](...)
  3. If no evaluator is found for the type, the condition returns false (fail-closed)

This fail-closed default means typos in condition types or missing evaluators safely prevent a variation from matching, rather than accidentally enabling it.

Interfaces and types ​

ConditionEvaluator ​

Every condition evaluator is a single async function:

Each evaluator receives a single args object and returns Promise<boolean>.

ParameterTypeDescription
conditionunknownRaw condition object from your definition
contextReadonly<Context>Resolution context
annotationsRecord<string, AnnotationValue>Mutable record — write metadata here during evaluation
depsReadonly<Dependencies>Injected runtime utilities (hash functions, fetchers)
depthstringDepth tracking string for nested conditions

Return true to match, false to skip the variation.

ConditionEvaluators ​

ConditionEvaluators is a Record<string, ConditionEvaluator> mapping condition type strings to evaluator functions.

registerEvaluators infers the condition key union from the input, so you get type-safe registration without an explicit key generic.

Writing and registering custom conditions ​

Write an async function that receives an args object containing the condition, context, and annotations, then returns a boolean. Use registerEvaluators to merge your evaluators with the built-ins — see the percentage rollout example below for a complete example.

registerEvaluators returns a new ConditionEvaluators map that includes all built-in evaluators plus yours. You cannot override the reserved composite types and and or — attempting to do so throws an error.

Passing evaluators ​

Pass your evaluators via the evaluators option:

ts
import { showwhat } from "showwhat";

const result = await showwhat({
  keys: ["new_checkout"],
  context: { env: "prod", userId: "user-42" },
  options: { data, evaluators: myEvaluators },
});

Or at the resolver level:

ts
import { resolve } from "showwhat";

const results = await resolve({
  definitions,
  context: { env: "prod", userId: "user-42" },
  options: { evaluators: myEvaluators },
});

Writing definitions with custom types ​

Custom condition types work in YAML/JSON definitions just like built-in ones. Any object with a type field that isn't a reserved built-in will pass schema validation:

yaml
definitions:
  new_checkout:
    variations:
      - value: true
        conditions:
          - type: percentage
            value: 25
      - value: false

Custom conditions compose with and/or and nest freely:

yaml
definitions:
  dark_mode:
    variations:
      - value: true
        conditions:
          - type: and
            conditions:
              - type: env
                value: prod
              - type: percentage
                value: 50
      - value: false

Fallback evaluator ​

If you need to handle unknown condition types gracefully — for example, in a preview or simulator — pass a fallback evaluator via options.fallback. When the resolver encounters a condition type with no registered evaluator, it calls the fallback instead of throwing.

ts
const result = await showwhat({
  keys: ["my_flag"],
  context: { env: "prod" },
  options: {
    data,
    fallback: async ({ condition, context }) => {
      return false;
    },
  },
});

Without a fallback, unknown condition types return a ResolutionError for the affected key.

Annotations ​

Evaluators can write metadata to the annotations record during evaluation. See Annotations for the full lifecycle, reading results, and the built-in annotations condition type.

Dependency injection ​

Custom evaluators often need runtime utilities — hash functions, async fetchers, lookup tables — that don't belong in the evaluation context. The deps parameter lets you inject these at the call site and access them in every evaluator.

Passing deps ​

Pass deps alongside context at any entry point:

ts
import { showwhat, registerEvaluators } from "showwhat";

const result = await showwhat({
  keys: ["new_checkout"],
  context: { env: "prod", userId: "user-42" },
  deps: { hash: murmurhash.v3 },
  options: { data, evaluators: myEvaluators },
});

deps is optional and defaults to {}. It is passed as Readonly to evaluators — evaluators should read from it, not mutate it.

Typed deps with evaluator contracts ​

Each evaluator can define an interface for the deps it requires and cast internally:

ts
// rollout-evaluator.ts
interface RolloutDeps {
  hash: (id: string) => number;
}

const rolloutEvaluator: ConditionEvaluator = async ({ condition, context, deps, annotations }) => {
  const { value } = condition as { type: "rollout"; value: number };
  const { hash } = deps as RolloutDeps;
  const userId = String(context.userId);
  const bucket = hash(userId) % 100;
  annotations.bucketId = bucket;
  return bucket < value;
};

Users can type-check their deps at the call site by annotating the variable:

ts
import type { RolloutDeps } from "./rollout-evaluator";
import type { SegmentDeps } from "./segment-evaluator";

// TypeScript validates the shape here
const deps: RolloutDeps & SegmentDeps = {
  hash: murmurhash.v3,
  fetchSegments: mySegmentFetcher,
};

const result = await showwhat({
  keys: ["feature"],
  context: myContext,
  deps,
  options: { data, evaluators },
});

deps vs context ​

contextdeps
ContainsEvaluation data (env, userId, region)Runtime utilities (functions, clients)
MutabilityReadonlyReadonly
Schema-validatedYesNo
Used by built-in evaluatorsYesNo

Summary ​

APIPurpose
ConditionEvaluatorType signature for evaluator functions
ConditionEvaluatorsEvaluators map — Record<string, ConditionEvaluator>
AnnotationsType for evaluator-written metadata
DependenciesType for injected runtime utilities
registerEvaluators(extra)Merge custom evaluators with built-ins
options.evaluatorsPass your evaluators to showwhat() or resolve()
depsPass runtime utilities to showwhat() or resolve()

Examples ​

Percentage rollouts ​

Percentage rollouts assign users to buckets deterministically. Inject the hash function via deps so your evaluator stays pure and testable:

ts
import type { ConditionEvaluator } from "showwhat";

interface RolloutDeps {
  hash: (id: string) => number;
}

const rolloutEvaluator: ConditionEvaluator = async ({ condition, context, deps }) => {
  const { value } = condition as { type: "percentage"; value: number };
  const { hash } = deps as RolloutDeps;
  const userId = context.userId;
  if (!userId) return false;
  return hash(String(userId)) % 100 < value;
};
ts
import murmurhash from "murmurhash";
import { showwhat, registerEvaluators } from "showwhat";

const myEvaluators = registerEvaluators({ percentage: rolloutEvaluator });

const result = await showwhat({
  keys: ["checkout_redesign"],
  context: { env: "prod", userId: "user-42" },
  deps: { hash: murmurhash.v3 },
  options: { data, evaluators: myEvaluators },
});
yaml
definitions:
  checkout_redesign:
    variations:
      - value: "variant-b"
        conditions:
          - type: env
            value: prod
          - type: percentage
            value: 10 # 10% rollout
      - value: "control"

TIP

showwhat handles the evaluation — tracking impressions, measuring conversion, and statistical analysis are your responsibility. Pipe results into your analytics tool of choice.

User targeting ​

A condition that targets users based on attributes from context:

ts
const myEvaluators = registerEvaluators({
  userAttribute: async ({ condition, context }) => {
    const { attribute, value } = condition as {
      type: "userAttribute";
      attribute: string;
      value: string | string[];
    };
    const actual = context[attribute];
    if (!actual) return false;
    const allowed = Array.isArray(value) ? value : [value];
    return allowed.includes(String(actual));
  },
});
yaml
definitions:
  premium_feature:
    variations:
      - value: true
        conditions:
          - type: userAttribute
            attribute: plan
            value: [pro, enterprise]
      - value: false

Next steps ​

  • Conditions for all built-in condition types
  • Custom Data Sources to pair custom evaluators with your own storage
  • Context for details on the context object and when to use deps vs context