Custom types
registry — the same set of builders reachable as T after initialize() — is exported so it can be extended before you ever call initialize:
import { initialize, registry } from "@ghostry/fabricator";
const types = registry.extend(({ T }) => ({
category: {
...T.enum.uniform(["electronics", "clothing", "food"]),
electronics: T.always("electronics"),
clothing: T.always("clothing"),
food: T.always("food"),
},
}));
const { T } = initialize({ types });
T.category; // a random category
T.category.electronics; // always "electronics".extend() deep-merges the object you return into the existing registry, so unrelated builders are untouched. Chain multiple .extend() calls to layer several additions.
The "namespace with a default" pattern
A custom type can be both usable bare (T.category) and hold named sub-variants (T.category.electronics). The way to get that is to spread an existing Schema and add properties alongside it — the same convention T.date.past/T.date.future and T.number.integer.sequence already use internally:
category: {
...T.enum.uniform(['electronics', 'clothing', 'food']),
electronics: T.always('electronics'),
clothing: T.always('clothing'),
food: T.always('food'),
}Bringing a dynamic data library
If that library is faker, reach for the faker extension rather than wiring it by hand — it is itself a registry.extend callback, so it composes exactly like anything on this page, and its builders draw from fabricator's own seed:
import { en } from "@faker-js/faker";
import { fakerExtension } from "@ghostry/fabricator-extension-faker-v10";
const types = registry
.extend(fakerExtension({ locale: en }))
.extend(({ T }) => ({
category: T.enum.uniform(["electronics", "clothing", "food"]),
}));
const { T } = initialize({ types });
T.faker.person.fullName();
T.category;For any other library — or for one-off logic — the pattern is a producer of your own:
const types = registry.extend(({ T }) => ({
person: {
fullName: T.string.as(() => faker.person.fullName()),
email: T.string.as(() => faker.internet.email()),
},
}));T.string.as(produce) layers an opaque, user-supplied producer on top of the string schema. produce is handed a { random, clock } context — the same shape T.opaque's producer gets — but nothing requires reading it: a call like faker.person.fullName() above draws from faker's own separate randomness regardless, so the field falls outside fabricator's seeded-reproducibility guarantee unless the producer explicitly consumes random itself. If that matters for your case, see T.opaque, where the producer's logic is the whole value, so using the given stream is the point rather than an option.
