Single Source of Truth Schemas
react-mnemonic has a first-class path for keeping runtime schema validation and TypeScript types aligned.
The goal is simple: define one schema object, then reuse it for:
- schema registry entries
defineMnemonicKey(...)- migration callbacks
useMnemonicKey(...)value inference
The pattern
import {
MnemonicProvider,
createSchemaRegistry,
defineKeySchema,
defineMnemonicKey,
defineMigration,
mnemonicSchema,
useMnemonicKey,
} from "react-mnemonic/schema";
const settingsV1 = defineKeySchema(
"settings",
1,
mnemonicSchema.object({
theme: mnemonicSchema.enum(["light", "dark"] as const),
}),
);
const settingsV2 = defineKeySchema(
"settings",
2,
mnemonicSchema.object({
theme: mnemonicSchema.enum(["light", "dark"] as const),
density: mnemonicSchema.enum(["compact", "comfortable"] as const),
}),
);
const settingsKey = defineMnemonicKey(settingsV2, {
defaultValue: {
theme: "light",
density: "comfortable",
},
reconcile: (value) => ({
...value,
density: value.density === "compact" ? "compact" : "comfortable",
}),
});
const registry = createSchemaRegistry({
schemas: [settingsV1, settingsV2],
migrations: [
defineMigration(settingsV1, settingsV2, (value) => ({
...value,
density: "comfortable",
})),
],
});
function SettingsPanel() {
const { value: settings, set } = useMnemonicKey(settingsKey);
return (
<button onClick={() => set({ ...settings, theme: settings.theme === "light" ? "dark" : "light" })}>
Theme: {settings.theme}
</button>
);
}
export default function App() {
return (
<MnemonicProvider namespace="app" schemaMode="default" schemaRegistry={registry}>
<SettingsPanel />
</MnemonicProvider>
);
}
What gets inferred
In the example above:
settingsKeyis inferred fromsettingsV2defaultValuemust match the schema shapereconcile(value)receives the decoded settings objectuseMnemonicKey(settingsKey)returns a fully typedvalueandsetdefineMigration(settingsV1, settingsV2, ...)forces the callback to return the v2 shape
That means the schema object does double duty:
- runtime: it is still a normal Mnemonic
JsonSchema - compile time: it carries a phantom TypeScript value type
Available schema helpers
The mnemonicSchema builder covers the built-in JSON Schema subset:
string(...)number(...)integer(...)boolean()nullValue()literal(value)enum([...])array(schema, ...)object({...})record(valueSchema)optional(schema)nullable(schema)
The returned schemas are plain JSON-compatible objects, so they can be stored in
KeySchema.schema without an adapter layer.
Tradeoffs
This path is stronger than plain defaultValue inference, but it is also more
intentional.
Use it when:
- a key is long-lived
- schema versioning matters
- migrations are part of the contract
- you want AI tools and teammates to see one obvious source of truth
Stay with the lightweight path when:
- the key is simple
- you do not need schema versioning
defaultValueinference is already enough
The lightweight path still works:
const themeKey = defineMnemonicKey("theme", {
defaultValue: "light" as "light" | "dark",
});