Learn
Type-safe A/B testing where the slot is the contract
Type-safe testing usually means a generated .d.ts that drifts the moment a flag changes. Apertio puts the contract in code: the slot definition is the single source of truth, and the types are the definition rather than a copy of it.
Type-safe A/B testing usually means a code-generation step. A tool reads your flags and writes a .d.ts, you import the generated types, and everything is typed until someone edits a flag without re-running the generator. Then the types describe a state your runtime no longer matches, and the safety was a snapshot, not a contract.
Apertio puts the contract in code and derives everything else from it. The slot definition is the single source of truth. There is no generated type file to regenerate and no drift between a .d.ts and reality, because the types are the definition rather than a copy of it.
Schema in code, values at runtime
The insight is splitting one word, "config," into two artifacts that move in opposite directions.
Schema is the slot definitions: this is a text slot, max 60 characters, default "Get started." It is the contract, the allowlist, and the types. It lives in code, engineering owns it, and it changes only on deploy.
Values are the actual headline marketing typed for variant B, the 50/50 split, the targeting. They do not live in code. They are runtime data, owned by marketing, fetched at resolution time.
You get all of the type safety from putting only the schema in code. Values inherit those types because they are validated against the in-code schema. You never put a value in code to make it type-safe.
One source of truth, nothing to regenerate
// slots.ts — one source of truth. No generated .d.ts, nothing to regenerate.
import { slot } from '@apertio/core'
export const slots = {
'pricing.cta.label': slot.text({
label: 'Pricing CTA',
default: 'Start free',
maxLength: 24,
}),
'pricing.plan.layout': slot.enum({
label: 'Plan layout',
options: ['table', 'cards'] as const,
default: 'cards',
}),
'pricing.trial.days': slot.number({
label: 'Trial length (days)',
default: 14,
min: 7,
max: 30,
integer: true,
}),
} // apertio.ts — typed hooks bound to the slots above, created once.
import { createApertio } from '@apertio/react'
import { slots } from './slots'
export const { useSlot, useSlotState } = createApertio(slots) // Consumption is fully typed from the definition above.
import { useSlot } from '../apertio'
const label = useSlot('pricing.cta.label') // string
const layout = useSlot('pricing.plan.layout') // 'table' | 'cards'
const days = useSlot('pricing.trial.days') // number
// useSlot('pricing.plan.colour') // type error: slot key does not exist The definition is the type. No generated file sits between them.
The definition is the type
useSlot('pricing.plan.layout') returns 'table' | 'cards' with autocomplete, derived from the options set. No generated file sits between the definition and your code.
Three artifacts, one source
Slot definitions produce runtime validators, the TypeScript type map, and a serializable descriptor that generates the marketing editor. Edit the definition and all three move together.
Validators back the types up
The same definition emits runtime validators that reject out-of-bounds values in the editor, at write time, and again at resolution. Types catch you at compile time; validators catch a bad value at runtime.
The editor is generated, so it cannot drift
A capped text input for text, a dropdown for enum, a bounded input for number. Marketing’s UI comes from the same definition your code consumes.
What type safety does and does not cover
Type safety bounds shape, not intent. A text slot capped at 60 characters guarantees a string within bounds. It does not guarantee the string is honest or on-brand. That is a content-governance problem (who can write values, approval workflows, audit logs), not a type-system problem, and Apertio treats it as a separate boundary.
And the modeling limitation applies here too. Only declared slots are typed and testable. A surface with no slot has no contract to inherit types from, so adding it is an engineering change. Type-safe testing covers everything you have modeled, not everything on the page.
Declare a slot, get the types
See how one slot definition produces validators, types, and the editor.
Frequently asked questions
- What makes A/B testing type-safe in Apertio?
- The slot definition is written in TypeScript and is the single source of truth. Your code consumes types derived directly from it, so useSlot('home.hero.layout') returns the exact union of options with autocomplete. There is no separate type file to keep in sync.
- Is the type safety a generated .d.ts file?
- No. A generated declaration file is a snapshot that drifts the moment a flag changes without regeneration. Apertio derives types from the live slot definition, so the types are the contract rather than a copy of it.
- How do marketing’s values stay type-safe if they live outside the code?
- Values are validated against the in-code schema, so they inherit its types and bounds. The same definition emits runtime validators applied in the editor, at write time, and again at resolution, so an out-of-bounds value is rejected or clamped.
- Does type safety stop a bad variant from shipping?
- It stops an out-of-shape one. Types and validators bound the shape of a value, not its intent. A type-valid headline can still be misleading, so content governance (roles, approvals, audit) is a separate layer.