API reference
Builder
Builder() is the one entry point. It has exactly three members — use, withConfig and for — and everything the library can do is reached by walking that chain to build().
Overview
The chain has five steps, and each one narrows the type of what comes next. Each narrowing is described under the method that performs it, below.
import { Builder } from '@maroonedog/luq';
import { requiredPlugin } from '@maroonedog/luq/plugins/required';
import { stringMinPlugin } from '@maroonedog/luq/plugins/stringMin';
import { numberMinPlugin } from '@maroonedog/luq/plugins/numberMin';
type User = {
name: string;
age: number;
};
const userValidator = Builder() // 1. start
.use(requiredPlugin) // 2. put plugins in the bag
.use(stringMinPlugin)
.use(numberMinPlugin)
.for<User>() // 3. name the type
.v('name', b => b.string.required().min(3)) // 4. declare fields
.v('age', b => b.number.required().min(18))
.build(); // 5. get { validate, parse, pick, pickAll }
const result = userValidator.validate({ name: 'Ada', age: 36 });
export const accepted = result.valid;build() returns an object
Not a function. Its four members are validate, parse, pick and pickAll, all covered on the Validator page.
Builder()
A factory. It takes no arguments and returns a fresh builder carrying an empty plugin bag.
use(plugin)
Adds one plugin to the bag and returns the same builder object, typed with the plugin folded in. The signature is use(plugin: AnyPlugin) — there is no variadic form, so chain one call per plugin.
import { Builder } from '@maroonedog/luq';
import { requiredPlugin } from '@maroonedog/luq/plugins/required';
type Draft = { title: string };
const draftValidator = Builder()
.use(requiredPlugin)
.for<Draft>()
// .min(3) is not offered here: stringMinPlugin was never used.
.v('title', b => b.string.required())
.build();
export const isTitled = draftValidator.validate({ title: 'x' }).valid;What use() does and does not do
- Registering the same plugin twice is first-wins, not an error.
- Two plugins claiming the same chain method is refused, rather than the later one silently overwriting the earlier as in 1.x.
use()mutates and returns the receiver, so two branches off one builder share plugins. If you want an immutable base, use a plugin registry.- Every
use()must come beforefor().
withConfig(config)
A per-builder override of the process-wide defaults. It is merged once at build() and reaches every plugin as RuleBuildContext.config, so nothing reads a mutable global while validation runs and two builders configured differently keep their own settings forever.
import { Builder } from '@maroonedog/luq';
import { requiredPlugin } from '@maroonedog/luq/plugins/required';
type Row = { note: string };
export const rowValidator = Builder()
.use(requiredPlugin)
.withConfig({ defaultSeverity: 'warning' })
.for<Row>()
.v('note', b => b.string.required())
.build();| Key | Type | Default |
|---|---|---|
defaultSeverity | 'error' | 'warning' | 'info' | 'error' |
messageKeyPrefix | string | '' |
toBooleanTruthyValues | readonly string[] | ['true', '1', 'yes', 'on'] |
numberFormat | { decimalSeparator, thousandSeparator } | '.' and ',' |
dateFormat | string | 'YYYY-MM-DD' |
trimStrings | boolean | false |
caseSensitive | boolean | true |
customTransforms | Record<string, (value: unknown) => unknown> | {} |
for<T>()
Names the type being validated and hands back a field builder. From here on v()'s path argument is a union computed from T.
import { Builder } from '@maroonedog/luq';
import { requiredPlugin } from '@maroonedog/luq/plugins/required';
type UserProfile = {
name: string;
preferences: {
theme: 'light' | 'dark';
notifications: boolean;
};
};
export const profileValidator = Builder()
.use(requiredPlugin)
.for<UserProfile>()
// 'preferences.theme' is offered by the type; 'preferences.colour' is not.
.v('preferences.theme', b => b.string.required())
.build();v(path, define, options?)
The one declaration mechanism. path is a literal string resolved against T, define receives the slot object and returns a chain, and the optional third argument configures the field — never a rule.
Field paths
import { Builder } from '@maroonedog/luq';
import { requiredPlugin } from '@maroonedog/luq/plugins/required';
import { stringMinPlugin } from '@maroonedog/luq/plugins/stringMin';
type Order = {
customer: { name: string };
items: { productId: string }[];
};
const orderValidator = Builder()
.use(requiredPlugin)
.use(stringMinPlugin)
.for<Order>()
.v('customer.name', b => b.string.required().min(2))
.v('items[*].productId', b => b.string.required().min(5))
.build();
const rejected = orderValidator.validate({
customer: { name: 'Acme' },
items: [{ productId: 'PROD-1' }, { productId: 'X' }],
});
// -> ['items[1].productId'] — the real index, never 'items[*].productId'
export const failedPaths = rejected.issues.map(issue => issue.path);Dots for nested fields, [*] for array elements. Every form, with what each one means: the path grammar.
No path enters an opaque leaf. Date, RegExp, Map, Set, Promise, Error, a buffer, a function and a class instance all stop the path, so 'when.getTime' does not compile.
Field options
Two keys, both about the value the field starts from. The default is substituted before any rule looks at the value, so validate() and parse() judge the same thing — only parse() writes it back into the returned object.
import { Builder } from '@maroonedog/luq';
import { optionalPlugin } from '@maroonedog/luq/plugins/optional';
type Settings = {
language: string;
createdAt: number;
};
export const settingsValidator = Builder()
.use(optionalPlugin)
.for<Settings>()
// A constant default.
.v('language', b => b.string.optional(), { default: 'en' })
// A factory default, and a null that is left alone.
.v('createdAt', b => b.number.optional(), {
default: () => Date.now(),
applyDefaultToNull: false,
})
.build();| Key | Type | Notes |
|---|---|---|
default | TValue | ((root: unknown) => TValue) | The factory form may read the object it is filling in. |
applyDefaultToNull | boolean | Defaults to true: a declared null is replaced. |
strict()
A type-level obligation with no runtime effect. If every leaf path of T has been declared, strict() returns the builder and the chain carries on. If any is missing it returns a MissingFieldsError instead, which has no build(), so the file stops compiling and the error names the paths you forgot.
import { Builder } from '@maroonedog/luq';
import { requiredPlugin } from '@maroonedog/luq/plugins/required';
import { stringMinPlugin } from '@maroonedog/luq/plugins/stringMin';
import { numberMinPlugin } from '@maroonedog/luq/plugins/numberMin';
type Account = {
id: string;
name: string;
age: number;
};
export const accountValidator = Builder()
.use(requiredPlugin)
.use(stringMinPlugin)
.use(numberMinPlugin)
.for<Account>()
.v('id', b => b.string.required())
.v('name', b => b.string.required().min(2))
.v('age', b => b.number.required().min(18))
.strict() // every leaf path is declared, so this returns the builder
.build();// Drop the .v('age', ...) line above and .strict() no longer returns a
// builder. It returns this instead, so .build() does not exist and the
// file does not compile:
//
// MissingFieldsError<'age'> = {
// luqError: 'missingFieldDeclarations';
// message: 'Declare every leaf path, or drop .strict().';
// missing: 'age';
// }It can sit anywhere in the chain, and you may keep adding rules after it.
Rejecting extra properties
strict() says nothing about properties that are not in the type. Rejecting those at run time is a rule, and it belongs to the objectAdditionalProperties plugin.
import { Builder } from '@maroonedog/luq';
import { requiredPlugin } from '@maroonedog/luq/plugins/required';
import { objectAdditionalPropertiesPlugin } from '@maroonedog/luq/plugins/objectAdditionalProperties';
type Envelope = {
user: { name: string; age: number };
};
const envelopeValidator = Builder()
.use(requiredPlugin)
.use(objectAdditionalPropertiesPlugin)
.for<Envelope>()
// Second argument is a plain string array, not an options object.
.v('user', b => b.object.required().additionalProperties(false, ['name', 'age']))
.v('user.name', b => b.string.required())
.v('user.age', b => b.number.required())
.build();
const rejected = envelopeValidator.validate({
user: { name: 'Ada', age: 36, nickname: 'A' },
});
export const accepted = rejected.valid; // falsebuild()
Compiles the declarations into a plan once and returns a Validator<T>. Building is the expensive half, so build once at module scope and reuse the object — the measured cost per shape.
import { Builder } from '@maroonedog/luq';
import { requiredPlugin } from '@maroonedog/luq/plugins/required';
import { stringEmailPlugin } from '@maroonedog/luq/plugins/stringEmail';
import { stringMinPlugin } from '@maroonedog/luq/plugins/stringMin';
type Profile = {
email: string;
displayName: string;
bio: string;
};
const profileValidator = Builder()
.use(requiredPlugin)
.use(stringEmailPlugin)
.use(stringMinPlugin)
.for<Profile>()
.v('email', b => b.string.required().email())
.v('displayName', b => b.string.required().min(2))
.v('bio', b => b.string.required())
.build();
// build() returns an OBJECT. It is not callable.
const whole = profileValidator.validate({
email: 'ada@example.com',
displayName: 'Ada',
bio: 'Analyst.',
});
const oneField = profileValidator.pick('displayName').validate('A');
const wizardStep = profileValidator
.pickAll(['email', 'displayName'])
.validate({ email: 'ada@example.com', displayName: 'Ada' });
export const outcomes = [whole.valid, oneField.valid, wizardStep.valid];| Member | What it gives back |
|---|---|
validate(value, options?) | ValidationResult<T> holding the original value. |
parse(value, options?) | ValidationResult<T> holding the value after transforms. |
pick(path) | A single-field validator for one declared path. |
pickAll(paths) | A validator returning exactly those paths, keyed by the path string. |
All four in detail: the Validator page.
Reusing a field rule
The same chain used on several fields is a FieldRule, minted from a plugin registry and spliced in with useField.
import { createPluginRegistry, useField } from '@maroonedog/luq/field-rule';
import { requiredPlugin } from '@maroonedog/luq/plugins/required';
import { stringEmailPlugin } from '@maroonedog/luq/plugins/stringEmail';
type Contact = {
email: string;
backupEmail: string;
};
const registry = createPluginRegistry()
.use(requiredPlugin)
.use(stringEmailPlugin);
const emailRule = registry.createFieldRule<string>(
b => b.string.required().email(),
{ name: 'email' }
);
// useField is a FREE FUNCTION taking the builder first.
const withEmail = useField(registry.toBuilder().for<Contact>(), 'email', emailRule);
export const contactValidator = useField(withEmail, 'backupEmail', emailRule).build();Signatures
| Call | Returns | Notes |
|---|---|---|
Builder() | Builder<{}> | Empty plugin bag. |
use(plugin) | Builder<B & BagEntry<P>> | One plugin per call. Mutates the receiver. |
withConfig(config) | Builder<B> | Merged once, at build(). |
for<T>() | FieldBuilder<T, B, never> | T extends object. |
v(path, define, options?) | FieldBuilder<T, B, TDeclared | K> | Declaration list grows in the type. |
strict() | FieldBuilder or MissingFieldsError | Compile-time only. |
build() | Validator<T> | { validate, parse, pick, pickAll } |
Removed since 1.x
| 1.x | Now |
|---|---|
use(a, b, c) | One plugin per use() call. |
builder.useField(path, rule) | useField(builder, path, rule) from @maroonedog/luq/field-rule. |
.field(path), .strictOnEditor() | Gone. Use .v() and .strict(). |
'items.name', 'items.*.name' | Write 'items[*].name'. |
'items[0].name' | A fixed index. Write 'items[*].name'. |
'tags.length' | Use .minLength() / .maxLength() on 'tags'. |
.v(path, define, 'en') | The bare-value shorthand is gone. Write { default: 'en' }. |
{ description }, { deprecated }, { metadata } | Removed from field options. Only default and applyDefaultToNull remain. |
registry.register(plugin) | registry.use(plugin), and it is immutable. |
Builder().fromJsonSchema(schema) | fromJsonSchema(schema), a free function. See JSON Schema. |