function
minLength
export function minLength(value: number): Step<string, string> { ... }At least value characters.
API reference
Schemas that parse rather than assert: typed inference, issue paths and JSON Schema export, part of the Unified Toolchain for Flow.
Written from the source by uf doc when this site was built: the signature and the comment above each export, grouped by the specifier a program imports it from.
@uniflowed/validator/actionfunction
minLengthexport function minLength(value: number): Step<string, string> { ... }At least value characters.
function
maxLengthexport function maxLength(value: number): Step<string, string> { ... }At most value characters.
function
lengthexport function length(value: number): Step<string, string> { ... }Exactly value characters.
function
nonEmptyexport function nonEmpty(): Step<string, string> { ... }At least one character.
Separate from minLength(1) because it is the check a form makes on every required text field, and nonEmpty() says why at the call site.
function
regexexport function regex(pattern: RegExp, message?: string): Step<string, string> { ... }A string matching pattern.
The pattern is tested against a reset lastIndex every time, because a caller who reaches for /g would otherwise get a schema that alternates between accepting and rejecting the same input.
function
emailexport function email(): Step<string, string> { ... }Something shaped like an email address.
Deliberately loose. The grammar in RFC 5322 accepts addresses no mail server will route and the regular expressions that implement it are famous for rejecting real ones; the only test that proves an address exists is sending something to it. This rejects the typos — a missing at-sign, a missing dot — and gets out of the way.
function
urlexport function url(): Step<string, string> { ... }A URL the platform's own parser accepts, so the parse is the check.
function
uuidexport function uuid(): Step<string, string> { ... }A UUID with a version and a variant, as RFC 9562 defines them.
function
isoDateexport function isoDate(): Step<string, string> { ... }A calendar date as YYYY-MM-DD.
The shape is not enough: 2026-02-30 matches the pattern and is not a day. The value is round-tripped through Date and compared back, which rejects every month that is shorter than the payload thought.
function
trimexport function trim(): Step<string, string> { ... }Whitespace off both ends, before whatever comes next in the pipeline.
function
minexport function min(value: number): Step<number, number> { ... }At least value.
function
maxexport function max(value: number): Step<number, number> { ... }At most value.
function
multipleOfexport function multipleOf(value: number): Step<number, number> { ... }A multiple of value.
The remainder is compared with a tolerance rather than against zero, because 0.3 % 0.1 is 0.09999999999999998 and a step of 0.1 on a price field is the reason anybody asks for this.
function
minItemsexport function minItems<TItem>(value: number): Step<$ReadOnlyArray<TItem>, $ReadOnlyArray<TItem>> { ... }At least value items. Arrays, where minLength is for strings.
function
maxItemsexport function maxItems<TItem>(value: number): Step<$ReadOnlyArray<TItem>, $ReadOnlyArray<TItem>> { ... }At most value items.
@uniflowed/validator/collectionfunction
arrayexport function array<TOutput, TInput>(
item: Schema<TOutput, TInput>,
): Schema<$ReadOnlyArray<TOutput>, $ReadOnlyArray<TInput>> { ... }Every item of an array, each parsed by item.
function
tupleexport function tuple<TItems extends Options>(
items: TItems,
): Schema<ItemsOutput<TItems>, ItemsInput<TItems>> { ... }A fixed number of positions, each with a schema of its own.
The arity is part of the type: a payload with one item too many is rejected rather than truncated, because a tuple whose length varied would be an array with extra steps.
function
recordexport function record<TOutput, TInput>(
value: Schema<TOutput, TInput>,
): Schema<{ readonly [string]: TOutput, ... }, { readonly [string]: TInput, ... }> { ... }An object whose keys are not known ahead of time.
Only own enumerable keys are read, so a payload carrying __proto__ or constructor cannot smuggle an inherited value into the parsed result.
function
mapexport function map<TKey, TKeyInput, TValue, TValueInput>(
key: Schema<TKey, TKeyInput>,
value: Schema<TValue, TValueInput>,
): Schema<$ReadOnlyMap<TKey, TValue>, $ReadOnlyMap<TKeyInput, TValueInput>> { ... }A Map, with a schema for its keys and one for its values.
Rebuilt rather than checked in place, because a key schema may be a pipe that changes the key — and a map re-keyed in place would collide with itself halfway through.
function
setexport function set<TOutput, TInput>(
item: Schema<TOutput, TInput>,
): Schema<$ReadOnlySet<TOutput>, $ReadOnlySet<TInput>> { ... }A Set, every member parsed by item.
A pipe on item can make two distinct inputs equal — trim() over "a" and "a " — and the rebuilt set then has one member where the input had two. That is what a set is for, and it is worth knowing before it surprises somebody counting rows.
@uniflowed/validator/infertype
InferOutputexport type InferOutput<TSchema> = TSchema extends Schema<infer TValue, infer TSource>
? TValue
: empty;The type a schema produces. The one every consumer wants.
type
InferInputexport type InferInput<TSchema> = TSchema extends Schema<infer TValue, infer TSource>
? TSource
: empty;The type a valid input to a schema has.
Equal to [InferOutput] until a transform is in the pipeline. Where they differ, this is the one a form's defaultValues wants and the other is the one its onValid receives.
type
Inferexport type Infer<TSchema> = InferOutput<TSchema>;The older name for [InferOutput], kept so existing annotations compile.
type
Shapeexport type Shape = { readonly [string]: Schema<mixed, mixed>, ... };An object whose values are schemas: what object and variant take.
type
Optionsexport type Options = $ReadOnlyArray<Schema<mixed, mixed>>;A list of schemas: what union and tuple take.
type
ShapeOutputexport type ShapeOutput<TShape extends Shape> = {
[Key in keyof TShape]: InferOutput<TShape[Key]>,
};The object a shape produces, key by key.
type
ShapeInputexport type ShapeInput<TShape extends Shape> = {
[Key in keyof TShape]: InferInput<TShape[Key]>,
};The object a shape accepts, key by key.
type
ItemsOutputexport type ItemsOutput<TItems extends Options> = {
[Index in keyof TItems]: InferOutput<TItems[Index]>,
};The tuple a list of schemas produces, position by position.
type
ItemsInputexport type ItemsInput<TItems extends Options> = {
[Index in keyof TItems]: InferInput<TItems[Index]>,
};The tuple a list of schemas accepts, position by position.
@uniflowed/validator/issuetype
Pathexport type Path = $ReadOnlyArray<string>;Where an issue happened, as object keys and array indices from the root.
type
PathBufferexport type PathBuffer = Array<string>;The mutable buffer the synchronous walk descends with.
schema.js explains why one array is pushed and popped rather than a fresh array being allocated per field. The type is separate from [Path] so that the distinction between "the buffer, which is being mutated right now" and "a path, which is a value" is visible in every signature.
type
Issueexport type Issue = {|
readonly code: string,
readonly message: string,
readonly path?: Path,
|};One reason a value was rejected.
code is for programs — "type", "min_length", "unknown_key" — and is stable across message changes, so a caller can tell "this is not an email address" from "we need an email address" without matching on prose.
path is absent rather than empty when the issue is about the whole value, because the overwhelmingly common case is a successful parse and an object with one fewer field is one fewer allocation on the path that matters.
function
issueexport function issue(code: string, message: string, path: Path): Issue { ... }An issue at wherever the walk currently is.
The slice is the only copy of a path the package makes, and it happens exactly when a value was going to be rejected anyway.
function
issueUnderexport function issueUnder(code: string, message: string, path: Path, keys: Path): Issue { ... }An issue at path with keys appended: a cross-field rule's landing spot.
class
ValidationErrorexport class ValidationError extends Error { ... }What [parse] raises.
A real Error subclass so it survives instanceof, logging and a catch that only knows about errors, and it carries issues so a caller can build a field-by-field response without parsing the message back apart.
type
FlatIssuesexport type FlatIssues = {|
readonly root: $ReadOnlyArray<string>,
readonly nested: { readonly [string]: $ReadOnlyArray<string>, ... },
|};Issues grouped the way a form renders them.
root is everything that was about the value as a whole; nested is keyed by the dotted path, which is the same string @uniflowed/form's register was given. Written through [put] because a payload's own __proto__ key reaches this function as a path segment.
function
flattenexport function flatten(issues: $ReadOnlyArray<Issue>): FlatIssues { ... }Group issues by their path, for a caller that renders per field.
@uniflowed/validator/json-schematype
JsonSchemaNodeexport type JsonSchemaNode = { readonly [string]: mixed, ... };One node of a JSON Schema document.
type
Unrepresentableexport type Unrepresentable = {|
readonly path: Path,
readonly kind: string,
|};Somewhere the document is less precise than the schema it came from.
type
JsonSchemaExportexport type JsonSchemaExport = {|
readonly schema: JsonSchemaNode,
readonly unrepresentable: $ReadOnlyArray<Unrepresentable>,
|};A JSON Schema document, and everything JSON Schema could not say.
function
toJsonSchemaexport function toJsonSchema(schema: Schema<mixed, mixed>): JsonSchemaExport { ... }Convert schema to a JSON Schema document.
The walk keeps three pieces of state — the definitions a recursive schema needs, the names already given out, and the places JSON Schema could not say what the schema meant — which is why it is a closure over a converter rather than a free function.
@uniflowed/validator/lazyfunction
lazyexport function lazy<TOutput, TInput = mixed>(
build: () => Schema<TOutput, TInput>,
): Schema<TOutput, TInput> { ... }A synchronous schema built on first use.
id is what a converter keys its definitions on: json-schema.js sees the same symbol every time it walks back around the cycle, which is how a recursive schema becomes a $ref rather than a stack overflow.
function
lazyAsyncexport function lazyAsync<TOutput, TInput = mixed>(
build: () => Schema<TOutput, TInput>,
): Schema<TOutput, TInput> { ... }A recursive schema with an asynchronous step somewhere inside it.
@uniflowed/validator/namespace@uniflowed/validator/objectfunction
objectexport function object<TShape extends Shape>(
shape: TShape,
): Schema<ShapeOutput<TShape>, ShapeInput<TShape>> { ... }An object with exactly the shape's keys; anything else is dropped.
function
strictObjectexport function strictObject<TShape extends Shape>(
shape: TShape,
): Schema<ShapeOutput<TShape>, ShapeInput<TShape>> { ... }An object that rejects keys the shape does not name.
The unknown-key scan runs whether or not the fields parsed. Returning early on a field failure meant { name: 1, extra: true } reported the wrong type of name and said nothing about extra, so fixing the first error revealed the second — which is the whole reason this validator collects issues instead of stopping at one.
function
looseObjectexport function looseObject<TShape extends Shape>(
shape: TShape,
): Schema<{ ...ShapeOutput<TShape>, ... }, { ...ShapeInput<TShape>, ... }> { ... }An object that keeps the keys the shape does not name.
The extra keys are in the parsed value and not in its type: they are mixed, because nothing validated them. The result type is inexact, which is Flow saying exactly that.
function
partialexport function partial<TShape extends Shape>(
shape: TShape,
): Schema<Partial<ShapeOutput<TShape>>, Partial<ShapeInput<TShape>>> { ... }Every field of shape, each allowed to be missing.
A key that was absent is present in the result holding undefined, rather than absent from it. One shape for the parsed value means a consumer reads draft.name without asking whether the key exists, and Partial says the same thing to the checker.
@uniflowed/validator/optionalfunction
optionalexport function optional<TOutput, TInput>(
schema: Schema<TOutput, TInput>,
): Schema<void | TOutput, void | TInput> { ... }undefined passes through; anything else goes to schema.
function
nullableexport function nullable<TOutput, TInput>(
schema: Schema<TOutput, TInput>,
): Schema<null | TOutput, null | TInput> { ... }null passes through; anything else goes to schema.
function
nullishexport function nullish<TOutput, TInput>(
schema: Schema<TOutput, TInput>,
): Schema<null | void | TOutput, null | void | TInput> { ... }Either null or undefined passes through, unchanged.
function
withDefaultexport function withDefault<TOutput, TInput>(
schema: Schema<TOutput, TInput>,
value: TOutput,
): Schema<TOutput, void | TInput> { ... }undefined becomes value; anything else goes to schema.
The default is not validated. It is a value the program wrote, in the program's own types, and running it back through the parser would only be a chance for the two to disagree.
function
fallbackexport function fallback<TOutput>(
schema: Schema<TOutput, mixed>,
value: TOutput,
): Schema<TOutput, mixed> { ... }A schema that never fails, substituting value when the inner one does.
For the boundary where a bad field should not sink the whole payload — a cached response, a user preference — and where the alternative is a safeParse and a hand-written if at every call site. Its input type is mixed, because that is the truth: it accepts everything.
@uniflowed/validator/parsefunction
safeParseexport function safeParse<TOutput>(schema: Schema<TOutput, mixed>, value: mixed): Result<TOutput> { ... }Parse into a result, so failure is a value rather than control flow.
function
parseexport function parse<TOutput>(schema: Schema<TOutput, mixed>, value: mixed): TOutput { ... }Parse, or raise a [ValidationError] carrying every issue found.
function
safeParseAsyncexport function safeParseAsync<TOutput>(
schema: Schema<TOutput, mixed>,
value: mixed,
): Promise<Result<TOutput>> { ... }[safeParse], for a schema with something to wait for.
function
parseAsyncexport function parseAsync<TOutput>(
schema: Schema<TOutput, mixed>,
value: mixed,
): Promise<TOutput> { ... }[parse], for a schema with something to wait for.
function
isexport function is(schema: Schema<mixed, mixed>, value: mixed): boolean { ... }Whether value would parse.
A boolean and not a type guard. Flow's value is T needs the predicate to be provable from the function's body, and here the proof is a closure the checker cannot see through; a guard would be a claim rather than a check. Narrow with [safeParse] and read result.value, which is the same information with the value attached.
function
parserexport function parser<TOutput>(schema: Schema<TOutput, mixed>): (value: mixed) => Result<TOutput> { ... }A schema as a standalone function.
safeParse(schema, value) needs both halves at the call site, which is fine where the schema is in scope and useless where it is not — a boundary that wants to validate what arrives takes a *function*, not a schema and an import of this package. parser(User) is that function, and it is why @uniflowed/fetch can check a response body without depending on the validator at all.
hook
useValidationexport hook useValidation<TOutput>(schema: Schema<TOutput, mixed>, value: mixed): Result<TOutput> { ... }Validate a value during render.
A hook rather than a plain call so the React Compiler memoises it with the rest of the component: re-rendering for an unrelated reason does not re-walk the payload. It is here rather than in a module of its own because it is [safeParse] at a render boundary and not a second idea — and because nothing in this package imports React to provide it.
@uniflowed/validator/pipetype
Stepexport type Step<TFrom, TTo> = <TInput>(Schema<TFrom, TInput>) => Schema<TTo, TInput>;One stage of a pipeline.
Polymorphic in TInput so that the type of the pipeline's *input* is carried through every step rather than being flattened to mixed at the first one.
type
Pipeexport type Pipe = {
<A, AIn>(schema: Schema<A, AIn>): Schema<A, AIn>,
<A, AIn, B>(schema: Schema<A, AIn>, a: Step<A, B>): Schema<B, AIn>,
<A, AIn, B, C>(schema: Schema<A, AIn>, a: Step<A, B>, b: Step<B, C>): Schema<C, AIn>,
<A, AIn, B, C, D>(
schema: Schema<A, AIn>,
a: Step<A, B>,
b: Step<B, C>,
c: Step<C, D>,
): Schema<D, AIn>,
<A, AIn, B, C, D, E>(
schema: Schema<A, AIn>,
a: Step<A, B>,
b: Step<B, C>,
c: Step<C, D>,
d: Step<D, E>,
): Schema<E, AIn>,
<A, AIn, B, C, D, E, F>(
schema: Schema<A, AIn>,
a: Step<A, B>,
b: Step<B, C>,
c: Step<C, D>,
d: Step<D, E>,
e: Step<E, F>,
): Schema<F, AIn>,
<A, AIn, B, C, D, E, F, G>(
schema: Schema<A, AIn>,
a: Step<A, B>,
b: Step<B, C>,
c: Step<C, D>,
d: Step<D, E>,
e: Step<E, F>,
f: Step<F, G>,
): Schema<G, AIn>,
<A, AIn, B, C, D, E, F, G, H>(
schema: Schema<A, AIn>,
a: Step<A, B>,
b: Step<B, C>,
c: Step<C, D>,
d: Step<D, E>,
e: Step<E, F>,
f: Step<F, G>,
g: Step<G, H>,
): Schema<H, AIn>,
<A, AIn, B, C, D, E, F, G, H, I>(
schema: Schema<A, AIn>,
a: Step<A, B>,
b: Step<B, C>,
c: Step<C, D>,
d: Step<D, E>,
e: Step<E, F>,
f: Step<F, G>,
g: Step<G, H>,
h: Step<H, I>,
): Schema<I, AIn>,
...
};pipe's type: one call signature per number of steps.
Inexact, because an exact object type with call properties cannot be inhabited by a function.
function
refineexport function refine<TOutput, TInput>(
schema: Schema<TOutput, TInput>,
accepts: (value: TOutput) => boolean,
code: string,
message: string,
constraint: Constraint,
at: Path = [],
): Schema<TOutput, TInput> { ... }A schema with one more thing that must be true of its output.
Every named step in action.js is a call to this. The constraint is what makes the step visible to json-schema.js: a refinement that cannot say what it refined is invisible to every exporter.
function
checkexport function check<TValue>(
accepts: (value: TValue) => boolean,
message: string,
at: Path = [],
): Step<TValue, TValue> { ... }An arbitrary predicate, with the message it should report.
Every other step in the package is a special case of this one. It exists so that a rule the library did not anticipate — a checksum, a business rule, one field agreeing with another — is a one-liner rather than a reason to abandon the schema and hand-roll validation.
at is where the issue lands, relative to the value being checked. See the module docs for the cross-field case it is there for.
function
checkAsyncexport function checkAsync<TValue>(
accepts: (value: TValue) => Promise<boolean>,
message: string,
at: Path = [],
): Step<TValue, TValue> { ... }A predicate that has to ask something.
"Is this username taken" is a question with a network on the other end, and a schema containing one is asynchronous from here up: the object around it, the array around that, and [safeParse] will refuse it and say to use [safeParseAsync].
function
transformexport function transform<TFrom, TTo>(change: (value: TFrom) => TTo): Step<TFrom, TTo> { ... }Change the value, and with it the schema's output type.
Runs after everything before it in the pipeline has accepted, so a transform never sees a value the steps above rejected — which is why pipe(string(), minLength(2), transform((text) => text.length)) is safe to write in that order and means something different in the other.
function
transformAsyncexport function transformAsync<TFrom, TTo>(
change: (value: TFrom) => Promise<TTo>,
): Step<TFrom, TTo> { ... }A transform that has to wait: a lookup, a hash, a decode off the main path.
function
brandexport function brand<TValue>(name: string): Step<TValue, TValue> { ... }A name on an otherwise ordinary value.
It checks nothing at run time and it is not pretending to. The name reaches the description, so an exported schema can say "this string is a UserId"; it does not reach the Flow type, because Flow's opaque types are declared in a module and cannot be produced by a call. infer.js says what to do instead when the distinction has to be enforced.
@uniflowed/validator/plain-objectfunction
isPlainObjectexport function isPlainObject(value: mixed): boolean { ... }Whether value is an object a shape or a record could be read from.
function
plainRecordexport function plainRecord(value: mixed): { readonly [string]: mixed, ... } { ... }value as something with string keys.
The single object boundary in the package. Every caller has already asked [isPlainObject], and every field that leaves a schema went through that schema first, so the mixed values this exposes are narrowed before they reach an output type.
function
ownKeysexport function ownKeys(value: { readonly [string]: mixed, ... }): $ReadOnlyArray<string> { ... }The own keys of value, and nothing inherited.
Object.keys already skips the prototype chain, which is why it is here rather than for (const key in value).
function
ownValueexport function ownValue(source: { readonly [string]: mixed, ... }, key: string): mixed { ... }Read one key, without falling through to the prototype.
record.constructor is Object on every object in the language; a shape with a constructor field would otherwise be handed a function and report that the payload was fine. It also makes object(shape) answer the same way for {} and for a class instance with getters on its prototype, which is the sort of difference that is discovered in production.
Object.hasOwn on every field read costs about 13% of a field-dense parse, measured on the workload in index.js. That is the price of the paragraph above and it is being paid deliberately: a payload out of JSON.parse has only own keys, so the check earns nothing there, and it earns everything the first time somebody hands a schema an object they built themselves.
function
putexport function put<Value>(out: { [string]: Value, ... }, key: string, value: Value): void { ... }Write one parsed field into the object being built.
__proto__ is the only key that needs defineProperty, and it is worth knowing why rather than reaching for it on every field. Object.prototype has exactly one accessor on it — __proto__ — and assignment to a key an accessor owns runs the setter instead of adding a property. Every other inherited name (constructor, toString, valueOf) is a *data* property, and assigning to one of those shadows it with an own property on the receiver, which is what a parsed field is supposed to be.
So one string comparison is the whole defence against a hostile payload, and defineProperty is the slow path for the one key that needs it. That is not a micro-optimisation: defineProperty on every field made a thousand-record parse three times slower than the same parse with an assignment in it — more than the entire rest of the walk cost — and the measurement is in index.js.
What this does not defend against is an Object.prototype that some other code has already given a setter to. Nothing in a parser can: a process whose Object.prototype is writable by an attacker has lost, and every read the consumer makes afterwards goes through the same polluted object.
@uniflowed/validator/primitivefunction
numberexport function number(): Schema<number, number> { ... }A finite number.
NaN and the infinities are rejected. They are numbers to typeof and disasters to arithmetic, and a validator that lets NaN through has not validated anything — every comparison downstream silently answers false.
function
bigintexport function bigint(): Schema<bigint, bigint> { ... }A bigint.
Separate from [number] because the two do not mix: 1n === 1 is false, 1n + 1 throws, and JSON.stringify refuses. A schema that accepted either would hand its caller a value whose arithmetic depends on the payload.
function
unknownexport function unknown(): Schema<mixed, mixed> { ... }Anything at all, unexamined. The identity of this package.
function
neverexport function never(): Schema<empty, empty> { ... }Nothing at all.
For the branch of a union that must not be reachable, and for a shape whose field is being removed: never() says so at the boundary instead of leaving a field that quietly still works.
function
null_export function null_(): Schema<null, null> { ... }Exactly null. Distinct from a missing key, which is [optional].
function
undefined_export function undefined_(): Schema<void, void> { ... }Exactly undefined.
function
enum_export function enum_<TValue extends string>(
values: $ReadOnlyArray<TValue>,
): Schema<TValue, TValue> { ... }One of a fixed list of strings.
The message names every option, because a rejected enum is almost always a typo and the fix is in the list the caller could not see.
function
dateexport function date(): Schema<Date, Date> { ... }A Date that is a date, rather than the Invalid Date a bad string makes.
function
instanceexport function instance<TValue>(ClassValue: Class<TValue>): Schema<TValue, TValue> { ... }An instance of ClassValue, by instanceof.
function
customexport function custom<TValue>(
accepts: (value: mixed) => boolean,
message: string,
name: string = "custom",
): Schema<TValue, TValue> { ... }A leaf this package has no name for.
The escape hatch, and the one place a caller's word is taken for a type: accepts returning true is what makes the value a TValue, and nothing checks that claim. Use it for a value with a shape of its own — a Uint8Array, a branded id from another library — and reach for [check] instead when the value is an ordinary type with a rule attached, because that keeps the type honest.
@uniflowed/validator/schematype
Resultexport type Result<out T> =
| {| readonly ok: true, readonly value: T |}
| {| readonly ok: false, readonly issues: $ReadOnlyArray<Issue> |};What a parse produced, or why it did not.
Covariant in T, which is what lets fail return a single Result<empty> and every caller accept it.
type
Descriptionexport type Description =
| {| readonly kind: "unknown" |}
| {| readonly kind: "never" |}
| {| readonly kind: "string" |}
| {| readonly kind: "number" |}
| {| readonly kind: "bigint" |}
| {| readonly kind: "boolean" |}
| {| readonly kind: "null" |}
| {| readonly kind: "undefined" |}
| {| readonly kind: "date" |}
| {| readonly kind: "instance", readonly name: string |}
| {| readonly kind: "custom", readonly name: string |}
| {| readonly kind: "literal", readonly value: string | number | boolean | null |}
| {| readonly kind: "enum", readonly values: $ReadOnlyArray<string> |}
| {| readonly kind: "array", readonly item: Description |}
| {| readonly kind: "tuple", readonly items: $ReadOnlyArray<Description> |}
| {| readonly kind: "record", readonly value: Description |}
| {| readonly kind: "map", readonly key: Description, readonly value: Description |}
| {| readonly kind: "set", readonly item: Description |}
| {|
readonly kind: "object",
readonly entries: $ReadOnlyArray<[string, Description]>,
readonly unknownKeys: "strip" | "reject" | "keep",
|}
| {| readonly kind: "union", readonly options: $ReadOnlyArray<Description> |}
| {|
readonly kind: "variant",
readonly key: string,
readonly branches: $ReadOnlyArray<[string, Description]>,
|}
| {| readonly kind: "intersect", readonly parts: $ReadOnlyArray<Description> |}
| {| readonly kind: "optional", readonly inner: Description |}
| {| readonly kind: "nullable", readonly inner: Description |}
| {| readonly kind: "nullish", readonly inner: Description |}
| {| readonly kind: "default", readonly inner: Description |}
| {| readonly kind: "fallback", readonly inner: Description |}
| {| readonly kind: "lazy", readonly id: symbol, readonly inner: () => Description |}
| {| readonly kind: "transformed", readonly inner: Description |}
| {|
readonly kind: "constrained",
readonly inner: Description,
readonly constraint: Constraint,
|};What a schema says about itself, for a consumer that has to write it down somewhere else.
Structural rather than nominal: a description is plain data with no schema inside it, so json-schema.js — or a generator that has not been written yet — can walk one without being able to run a parse. lazy is the exception and has to be, because a recursive value has no finite spelling; its id is the identity a converter keys its $defs on.
type
Constraintexport type Constraint =
| {| readonly kind: "minLength", readonly value: number |}
| {| readonly kind: "maxLength", readonly value: number |}
| {| readonly kind: "length", readonly value: number |}
| {| readonly kind: "minItems", readonly value: number |}
| {| readonly kind: "maxItems", readonly value: number |}
| {| readonly kind: "min", readonly value: number |}
| {| readonly kind: "max", readonly value: number |}
| {| readonly kind: "integer" |}
| {| readonly kind: "multipleOf", readonly value: number |}
| {| readonly kind: "pattern", readonly source: string |}
| {| readonly kind: "format", readonly name: string |}
| {| readonly kind: "brand", readonly name: string |}
| {| readonly kind: "opaque", readonly label: string |};What one pipe step narrowed.
opaque is the honest answer for [check]: an arbitrary predicate has no spelling in any export format, and saying so is better than emitting a schema that claims the value is unconstrained.
opaque-type
Schemaexport opaque type Schema<out TOutput, out TInput = mixed>: SchemaCarrier<
TOutput,
TInput,
> = SchemaCarrier<TOutput, TInput>;A parser from mixed to TOutput, whose valid inputs are TInput.
Opaque with a supertype bound rather than fully opaque, and the difference is what makes this package more than one file. Fully opaque, object.js could not read the kernel out of a schema primitive.js built, so every builder would have to live beside the type — which is the argument @uniflowed/effect makes for staying in one module, and it is a real one. The bound splits the guarantee in two: any module may *read* a schema, and only this one may *mint* one, because SchemaCarrier is not exported and [makeSchema] is the only thing that returns the opaque type. An application still cannot forge a schema, hand-write a kernel, or depend on the carrier's shape.
TInput defaults to mixed so that Schema<User> keeps meaning "a schema that produces a User" for the callers that only care about the output — @uniflowed/form's resolver, @uniflowed/fetch's response parser — and keeps compiling unchanged.
function
makeSchemaexport function makeSchema<TOutput, TInput>(
parse: (mixed, PathBuffer) => Result<TOutput>,
description: () => Description,
): Schema<TOutput, TInput> { ... }Mint a synchronous schema. The only way a Schema comes into existence.
function
makeAsyncSchemaexport function makeAsyncSchema<TOutput, TInput>(
parseAsync: (mixed, PathBuffer) => Promise<Result<TOutput>>,
description: () => Description,
): Schema<TOutput, TInput> { ... }Mint an asynchronous schema.
Its parse is null, which is what [run] refuses and what a composite reads to decide it is asynchronous too.
function
isAsyncexport function isAsync(schema: Schema<mixed, mixed>): boolean { ... }Whether schema needs [safeParseAsync]. Decided when it was built.
function
describeexport function describe(schema: Schema<mixed, mixed>): Description { ... }What schema accepts, as data. See [Description].
function
runexport function run<T>(schema: Schema<T, mixed>, value: mixed, path: PathBuffer): Result<T> { ... }Run schema where the walk currently is. Throws if schema is async.
function
runAtexport function runAt<T>(
schema: Schema<T, mixed>,
value: mixed,
path: PathBuffer,
key: string,
): Result<T> { ... }Run schema one step deeper in the path.
The push/pop pair is why the buffer stays balanced even when a nested schema returns early: nothing between them can throw except user code inside a transform or a check, and a schema that raised has already failed the whole parse.
function
runAsyncexport function runAsync<T>(
schema: Schema<T, mixed>,
value: mixed,
path: PathBuffer,
): Promise<Result<T>> { ... }Run schema, awaiting it if it is asynchronous and calling it if it is not.
function
runUnderexport function runUnder<T>(
schema: Schema<T, mixed>,
value: mixed,
path: PathBuffer,
keys: Path,
): Result<T> { ... }Run schema several steps deeper.
map is the caller that needs more than one segment: an entry's key and its value are two different places to fail, and ["3", "key"] says which without inventing a punctuation the rest of the package would have to parse.
function
runAtAsyncexport function runAtAsync<T>(
schema: Schema<T, mixed>,
value: mixed,
path: PathBuffer,
keys: Path,
): Promise<Result<T>> { ... }Run schema deeper, on a path of its own. See the module docs.
type
Jobexport type Job = {|
readonly keys: Path,
readonly schema: Schema<mixed, mixed>,
readonly value: mixed,
|};One child of a composite, for [collectAsync].
type
Collectedexport type Collected =
| {| readonly ok: true, readonly values: $ReadOnlyArray<mixed> |}
| {| readonly ok: false, readonly issues: $ReadOnlyArray<Issue> |};Every child's outcome, in the order the jobs were given.
function
collectAsyncexport function collectAsync(
jobs: $ReadOnlyArray<Job>,
path: PathBuffer,
): Promise<Collected> { ... }Run every child at once, and report all of their issues.
The one function that keeps object, array, tuple, record, map and set from each growing a second copy of the same loop: their asynchronous halves differ only in how they name their children and what they build out of the answers.
Promise.all rather than a loop of awaits, because a form whose two fields each ask a server should cost one round trip. Issues still come back in child order, because the results are folded in the order they were requested rather than the order they settled.
function
okexport function ok<T>(value: T): Result<T> { ... }A successful result.
function
failexport function fail(code: string, message: string, path: Path): Result<empty> { ... }A result carrying one issue, at wherever the walk is.
function
mergeIssuesexport function mergeIssues(issues: Array<Issue>, result: Result<mixed>): void { ... }Append result's issues to issues, if it has any.
@uniflowed/validator/unionfunction
unionexport function union<TOptions extends Options>(
options: TOptions,
): Schema<InferOutput<TOptions[number]>, InferInput<TOptions[number]>> { ... }The first schema that accepts the value.
When none do, every branch's issues are reported, because there is no way to know which branch the author meant. That is also why [variant] exists.
function
variantexport function variant<TBranches extends Shape>(
key: string,
branches: TBranches,
): Schema<InferOutput<TBranches[keyof TBranches]>, InferInput<TBranches[keyof TBranches]>> { ... }A union chosen by the value of one key.
The discriminant is read first and the matching branch is the only one run. A discriminant that is missing, is not a string, or names no branch is reported at the discriminant's own path, so a form can put the message on the control that chooses it.
function
intersectexport function intersect<TLeftOut, TLeftIn, TRightOut, TRightIn>(
left: Schema<TLeftOut, TLeftIn>,
right: Schema<TRightOut, TRightIn>,
): Schema<TLeftOut & TRightOut, TLeftIn & TRightIn> { ... }Both schemas, over the same value.
Binary rather than variadic: intersect(intersect(a, b), c) is the third one, the type is A & B with nothing for the checker to fold, and there is no arity table to keep in step with an implementation.
Both sides run, and both sides' issues are reported, for the same reason object does not stop at the first bad field.
What the result *is* depends on what the two produced. Two plain objects are merged, with the right-hand side winning a shared key — which is what makes intersect(object(base), object(extra)) mean what it looks like. Two identical values are that value. Anything else is a intersect issue rather than a guess, because there is no defensible way to merge a Date with a string and pretend the result satisfies both.