miqro · @miqro/core · @miqro/parser · @miqro/query · @miqro/jsx · @miqro/jsx-dom · @miqro/jsx-node · @miqro/request · @miqro/runner · @miqro/test · @miqro/test-http
Schema validation and coercion with full TypeScript inference.
import { Parser } from "@miqro/parser";
const parser = new Parser();
// Type is inferred automatically from the schema
const result = parser.parse({ id: "1", title: "hello" }, {
type: "object",
properties: {
id: "integer",
title: "string",
published: "boolean?"
}
});
result.id // number (coerced from string)
result.title // string
result.published // boolean | undefined
To declare a schema separately and keep inference, use as const satisfies Schema:
import { Parser, Schema } from "@miqro/parser";
const PostSchema = {
type: "object",
properties: { id: "integer", title: "string", published: "boolean?" }
} as const satisfies Schema;
const post = parser.parse(raw, PostSchema);
post.id // number
post.published // boolean | undefined
as const keeps the literal types the inference reads, and satisfies Schema checks the
schema is well formed without widening it. The const modifier on parse only applies to
schemas written inline at the call site, which is why a schema stored in a variable needs the
as const itself.
Two spellings that look equivalent and are not:
const PostSchema: Schema<Post> = { ... }; // ✗ unsound
const post = parser.parse(raw, PostSchema); // typed Post — even if the schema
// describes entirely different properties
Annotating with Schema<Post> makes inference short-circuit to Post without checking that
the schema produces one. post.id then compiles cleanly and is undefined at runtime.
const PostSchema = { ... }; // ✗ inference degrades
const PostSchema = { ... } satisfies Schema; // ✗ inference degrades
const post = parser.parse(raw, PostSchema); // typed unknown
Without as const the property strings widen to string, and there is nothing left to infer
from.
"string" required string
"string?" optional string (undefined if absent)
"number" required number
"integer" required integer (coerced)
"boolean" required boolean ("true"/"false" coerced)
"string[]" array of strings
"string[]?" optional array of strings
"string[]!" force single value into array, always returns string[]
"string[]!?" optional force-array
"string|number" string or number (first match wins)
"string|number?" string or optional number
string, string1 (non-empty), number, integer, boolean, date
url, email, regex, regexp, function, any
object, array, dict, enum
decodeHTML, encodeHTML, schema
Returns a Date. Accepts a Date, epoch milliseconds, or a string in the ECMAScript
Date Time String Format — and nothing else.
parser.parse("2026-07-30", "date"); // 2026-07-30T00:00:00.000Z
parser.parse("2026-07-30T12:00:00.000Z", "date"); // that instant
parser.parse(1785412800000, "date"); // from epoch ms
It rejects what new Date(...) quietly accepts: null (the epoch), true (1 ms after
it), "", unparseable strings (an Invalid Date object rather than an error), and a day
that does not exist. Date range checks the month but not the day, so both of these roll
over to March 2 through it and both throw here:
parser.parse("2026-02-30", "date"); // throws
parser.parse("2026-02-30T00:00:00.000Z", "date"); // throws
A date-only string is UTC midnight, built from its own components, so one calendar day is one instant on every host:
parser.parse("2026-07-30", "date"); // 2026-07-30T00:00:00.000Z in every timezone
parser.parse("2026-07", "date"); // 2026-07-01T00:00:00.000Z
parser.parse("2026", "date"); // 2026-01-01T00:00:00.000Z
Spellings outside the format are refused rather than read in the host's timezone. All
three of these parse in V8, all three as local time — "2026/07/30" in Asia/Tokyo is the
day before:
parser.parse("2026/07/30", "date"); // throws
parser.parse("Jul 30 2026", "date"); // throws
parser.parse("2026-07-30 12:00:00", "date"); // throws — note the space
A date-time string carries whatever zone it states (Z, +02:00, -0200). With no
zone it is local time, which is what the format says it means:
parser.parse("2026-07-30T12:00:00Z", "date"); // that instant everywhere
parser.parse("2026-07-30T12:00:00", "date"); // local — depends on the host
url accepts any protocol new URL() accepts, including javascript:, data: and file:.
When the value becomes a redirect target or a fetch destination, say which protocols are
allowed:
parser.parse(raw, { type: "url", urlProtocols: ["http:", "https:"] });
It returns a URL instance at runtime, but infers as unknown: TypeMap cannot name
URL without the dom lib, so it declares url: unknown, and that declaration cannot be
augmented away — TypeScript answers TS2717: Property 'url' must be of type 'unknown'.
Narrow at the call site instead:
const UrlSchema: Schema<URL> = { type: "url", urlProtocols: ["http:", "https:"] };
parser.parse(raw, UrlSchema); // URL
parser.parse(raw, "url") as URL; // or cast
const result = parser.parse(raw, {
type: "object",
properties: {
name: "string",
age: "integer?",
address: {
type: "object?",
properties: {
city: "string",
zip: "string?"
}
},
tags: {
type: "array",
arrayType: "string"
}
}
});
result.name // string
result.age // number | undefined
result.address?.city // string
result.tags // string[]
mode: "no_extra" (default) throws if unknown properties are present
mode: "remove_extra" strips unknown properties, returns defined shape only
mode: "add_extra" keeps unknown properties, type is defined shape & Record<string, unknown>
const result = parser.parse(raw, {
type: "object",
mode: "add_extra",
properties: { id: "integer" }
});
result.id // number
result["anything"] // unknown
// array of primitives
parser.parse(raw, { type: "array", arrayType: "string" }); // string[]
// array of objects — define element shape with co-located properties
const result = parser.parse(raw, {
type: "array",
arrayType: "object",
properties: {
id: "integer",
label: "string"
}
});
result[0].id // number
result[0].label // string
// array of arrays
parser.parse(raw, { type: "array", arrayType: "number[]" }); // number[][]
const result = parser.parse(raw, {
type: "enum",
enumValues: ["a", "b", "c"]
});
// result → "a" | "b" | "c"
// dict of primitives
parser.parse(raw, { type: "dict", dictType: "number" });
// → Record<string, number>
// dict of objects — define value shape with co-located properties
const result = parser.parse(raw, {
type: "dict",
dictType: "object",
properties: { x: "string", y: "number?" }
});
result["key"].x // string
result["key"].y // number | undefined
Aliases: Dict<string> and dict<string> are equivalent as type strings, as are
Array<string> and string[] (and Array<string>! and string[]!).
A parsed dict is created with Object.create(null), so it has no prototype — spread,
Object.keys and JSON.stringify work, result.hasOwnProperty(...) does not. That is
deliberate: a "__proto__" key on a plain object literal goes through the inherited
accessor and replaces the result's prototype instead of creating a property.
parser.parse(raw, { type: "string", allowNull: true }); // string | null
parser.parse(raw, { type: "enum", enumValues: ["a", "b"], allowNull: true }); // "a" | "b" | null
Beyond type and properties, a schema object takes:
required false makes the value optional — same as a "?" suffix
defaultValue returned when the value is undefined (and makes the key non-optional)
parseJSON JSON.parse a string value first; throws if the value is not a string
forceArray wrap a non-array value in an array — same as a "!" suffix
allowNull accept null and return it unchanged
mode object only — extra-property handling, see above
name plumbing only — always overwritten by parse()'s third argument
(and by the derived path when nested), so writing it does nothing
usage replaces the error message on any failure, missing values included
description free text, carried through for doc generation
title free text, carried through for doc generation
regex string | RegExp — required by "regex", and honoured by "string" too
stringMinLength string, string1, email, url
stringMaxLength
numberMin number, integer
numberMax
numberMinDecimals
numberMaxDecimals
arrayMinLength array
arrayMaxLength
arrayType array element type
dictType dict value type
enumValues enum
urlProtocols url
parser.parse(undefined, { type: "string", defaultValue: "anon" }); // "anon"
parser.parse('{"a":1}', { type: "object", parseJSON: true,
properties: { a: "integer" } }); // { a: 1 }
parser.parse("AB1", { type: "string", regex: "^[a-z]+$",
usage: "must be lowercase letters" }); // throws that message
Use InferSchema<S> to derive the TypeScript type from a schema definition
without calling parse:
import { InferSchema } from "@miqro/parser";
type T1 = InferSchema<"string?">; // string | undefined
type T2 = InferSchema<"number[]">; // number[]
type T3 = InferSchema<{ type: "object"; properties: { x: "string" } }>; // { x: string }
type T4 = InferSchema<{ type: "enum"; enumValues: readonly ["a","b"] }>; // "a" | "b"
type T5 = InferSchema<{ type: "number"; allowNull: true }>; // number | null
import { Parser } from "@miqro/parser";
const parser = new Parser();
parser.register("uuid", (value) => {
if (typeof value === "string" && /^[0-9a-f-]{36}$/.test(value)) {
return value;
}
// return undefined or throw ParseOptionsError to fail validation
});
parser.parse("123e4567-...", "uuid"); // → unknown
register also accepts a Schema or another type name in place of the function, and
takes aliases:
parser.register("posId", { type: "integer", numberMin: 1 }); // schema
parser.register("token", "string1", ["Token"]); // alias + aliases
Registering a name twice throws, and a name containing | or ? is rejected. Every
registered type T automatically gains T[], T[]! and dict<T> (suppress with the
noList / noDict parameters).
To get type inference for custom parsers, augment TypeMap in the module where it is
declared — note the .js extension:
declare module "@miqro/parser/build/built-in-parsers.js" {
interface TypeMap {
uuid: string;
}
}
parser.parse("123e4567-...", "uuid"); // → string
parser.parse(raw, "uuid[]"); // → string[]
parser.parse(raw, "dict<uuid>"); // → Record<string, string>
The extension is required under moduleResolution: "nodenext"; without it TypeScript
reports TS2664: Invalid module name in augmentation and the types stay unknown.
Augmenting "@miqro/parser" instead reports nothing at all and also has no effect —
TypeMap is declared in built-in-parsers, and that is the module the augmentation has
to name.
Every failure throws ParseOptionsError. argAttr is the dotted path to the property
that failed, built from the third argument to parse:
import { ParseOptionsError } from "@miqro/parser";
try {
parser.parse({ a: { b: "x" } }, {
type: "object",
properties: { a: { type: "object", properties: { b: "integer" } } }
}, "body");
} catch (e) {
if (e instanceof ParseOptionsError) {
e.name // "ParseOptionsError"
e.message // "body.a.b not number min decimals[0] max decimals[0]"
e.argAttr // "body.a.b"
}
}
Omit the third argument and the paths start at the property name (a.b).
Path helpers, exported alongside Parser. Both reject __proto__, __prototype__,
constructor and prototype anywhere in the path.
import { set, get } from "@miqro/parser";
set({}, "a.b.c", 1); // { a: { b: { c: 1 } } } — creates intermediates
get({ a: { b: { c: 1 } } }, "a.b.c"); // 1
get({}, "x.y", "fallback"); // "fallback"
set({}, "__proto__.x", 1); // throws Error: invalid attrPath
An array path (["a", "b", "c"]) works the same as a dotted string.