Skill v1.0.0
currentLLM-judged scan90/100version: "1.0.0" name: tsentials-validation description: Use when validating a model with fluent, eager, Result-based rules — Validator.validate with rules.for('path').notEmpty().maxLength(), nested paths and forEach for collections, cascade modes, subclassable Validator<T>, validateAsync with mustAsync and AbortSignal, validateWith as a railway step.
tsentials/validation
Fluent model validation that returns Result<T>. Rules run eagerly; the success value is the validated instance. Subpath-only: not exported from the root tsentials entry.
Installation
npm install tsentials
Import
import { Validator, CascadeMode } from 'tsentials/validation';import type { RuleContext, RuleChain, Configure, AsyncConfigure } from 'tsentials/validation';
Inline validation
type User = { name: string; email: string; address: { city: string }; items: { sku: string }[] };const r = Validator.validate(user, (rules) => {rules.for('name').notEmpty().maxLength(50);rules.for('email').notEmpty().emailAddress();rules.for('address.city').notEmpty(); // dotted paths are type-checkedrules.forEach('items', (item) => {item.for('sku').notEmpty();});});// r: Result<User>// failure codes: 'name.NotEmpty', 'email.EmailAddress', 'address.city.NotEmpty', 'items[0].sku.NotEmpty'
Error code format is <path>.<Rule>; all errors have type Validation.
Validator subclass
class UserValidator extends Validator<User> {protected configure(rules: RuleContext<User>) {rules.for('name').notEmpty().cascade(CascadeMode.Continue).minLength(3).maxLength(50);}}const validator = new UserValidator();validator.validate(user); // Result<User>validator.toRule(); // Rule<User> — plug into RuleEngine (tsentials/rules)
Cascade and null handling
- Default cascade is
Stop: the first failure on a chain skips the remaining rules of that chain. Use.cascade(CascadeMode.Continue)to accumulate. null/undefinedare skipped by every built-in rule exceptnotNull,notEmptyandequal. Guard optional values withnotEmpty()first.
Built-in rules (on rules.for(...))
| Group | Rules | |
|---|---|---|
| Presence | notNull, null, notEmpty | |
| String | maxLength, minLength, length, emailAddress, matches, contains, startsWith, endsWith | |
| Comparison | equal, notEqual, greaterThan, greaterThanOrEqualTo, lessThan, lessThanOrEqualTo, inclusiveBetween, exclusiveBetween | |
| Collection | minCount, maxCount, countBetween | |
| Enum | isDefinedEnum, isOneOf, hasOnlyDefinedFlags | |
| Custom | must(pred), must(pred, code, message), satisfies(rule), setValidator(v) |
rules.forEach(path, configure) validates each element; rules.self() validates the model itself; rules.addFailure(code, description) adds an error directly.
Async validation
const r = await Validator.validateAsync(user,async (rules, _model, signal) => {await rules.for('email').mustAsync(async (e) => await isFree(e, signal), 'Email.Taken', 'Already used');},{ signal },);
Await every async rule (mustAsync, forEachAsync, setValidatorAsync, satisfiesAsync) inside the callback. validate() throws TypeError if configure returns a promise or leaves an async rule un-awaited.
Railway step
import { Result } from 'tsentials/result';const checked = Validator.validateWith(Result.success(user), (rules) => {rules.for('name').notEmpty();});// failure input passes through unchanged; also: Validator.validateWithAsync
Patterns and pitfalls
- Import from
tsentials/validation— never from the roottsentialsentry. - AbortSignal convention: the caller's abort makes the promise reject with
signal.reason; it never becomes a validation error. An exception thrown by an async rule predicate or byconfigurerejects too (never a failure); usemustAsyncpredicates that returnfalsefor validation failures. - Subclasses implement
configure(rules)as a sync method; usevalidateAsyncwith anAsyncConfigurefor async rules. - Errors use
description, notmessage:r.errors[0]?.description. - A message argument may be a string or an
AppError.