@webergency-utils/typechecker
v0.8.0
Published
TypeScript compiler plugin for runtime validation
Downloads
2,996
Readme
@webergency-utils/typechecker
An ahead-of-time (AOT) TypeScript validation engine that compiles types into optimized runtime validators via a compiler transformer—no runtime reflection and no third-party schema library.
TL;DR
import { validate, constraint, format } from '@webergency-utils/typechecker';
interface User {
id: string & format.UUID;
name: string & constraint.MinLength<2>;
age: number & constraint.Minimum<18>;
}
const input: unknown = {
id: '550e8400-e29b-41d4-a716-446655440000',
name: 'Alice',
age: 25,
};
// Validate input against the User type definition
const result = validate<User>(input);
if (result.success) {
// TypeScript narrows type to User here
console.log('User is valid:', result.data);
} else {
console.error('Validation failed:', result.errors);
}Installation & Setup
This package is a TypeScript compiler plugin (transformer). You must compile with a compiler patcher such as ts-patch so the transformer can hook into tsc.
Peer dependency: typescript >=5.0.0 (required; provides the compiler API the transformer and language service plugin use).
There are no runtime dependencies—only the peer typescript and your build tooling.
1. Install Dependencies
Install the core package, along with ts-patch as a development dependency:
npm install @webergency-utils/typechecker
npm install --save-dev ts-patch2. Inject Compiler Hook
Run the patcher command to set up ts-patch inside your local TypeScript installation:
npx ts-patch install[!NOTE] It is recommended to add
ts-patch installto yourpackage.jsonpreparescript so it runs automatically after every dependency installation.
3. Configure tsconfig.json
Register the typechecker transformer (and optionally the language service plugin) under compilerOptions.plugins in tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"plugins": [
{ "transform": "@webergency-utils/typechecker/transformer" },
{ "name": "@webergency-utils/typechecker/plugin" }
]
}
}The transform entry is required for AOT validation. The name entry enables IDE constraint diagnostics via the language service plugin.
Architecture & Internals
The package utilizes a custom TypeScript compiler transformer and language service plugin to deliver highly efficient type-safe runtime validations.
Build-Time Compilation Flow
graph TD
A[TypeScript Source Code] --> B[ts-patch / Compiler Hook]
B --> C[TypeScript compiler plugin / Transformer]
C --> D[Extract types via compiler TypeChecker]
D --> E[Generate optimized JS validator function]
E --> F[Hoist file-local __val_hash / __schema_hash constants]
F --> G[Replace typed calls with __tcRuntime.validate / is / assert]
G --> H[Emit optimized JavaScript files]- Build-Time Transformation: The compiler transformer intercepts typed helper calls (
validate,is,assert,assertGuard,jsonSchema,parse,stringify,serializer). Schema helpers (validateSchema,isSchema,assertSchema,assertGuardSchema) are plain runtime APIs and do not require transformation. - Type Extraction & Analysis: It parses the target TS type structure, extracting intersection constraints, formats, transforms, and defaults recursively.
- File-local validators: The transformer generates highly optimized JavaScript validator functions for each resolved type shape, names them with a structural hash (
__val_<hash>/__schema_<hash>), and hoists them in the same file. - Call Replacement: Typed calls are rewritten to
__tcRuntime.validate(__val_<hash>, …)(and the matchingis/assert/assertGuardhelpers) with no global registry lookup.
External dependencies
- Required peer:
typescript(>=5.0.0) — compiler API for the transformer and optional language service plugin. - Required build tooling:
ts-patch(or equivalent) — patchestscsocompilerOptions.pluginstransformentries run. - Runtime dependencies: none.
Static Constraint Diagnostics
The package includes an IDE / Language Service plugin that statically checks literal values against type constraints during editing or compilation:
import { constraint } from '@webergency-utils/typechecker';
// This yields a compilation error directly in the IDE:
// Type '5' is not assignable to type 'number & Minimum<18>'.
const age: number & constraint.Minimum<18> = 5;Glossary
validate: Validates a value against a type, returning a structured result containing the validation status and a detailed list of errors.is: A type guard forT. Always mutates in place;frommay coerce nested fields. Root replacement fails the guard.assert: Validates a value and returns it, throwing a validation error on failure (supportsfromcoercion).assertGuard: Asserts a value isT. Always mutates in place;frommay coerce nested fields. Root replacement throws.serializer: AOT macro that compiles a(input: T) => stringserializer for JSON or query strings.transform/replacerare closed over at create time.stringify: AOT macro that validates and serializes a value as typeT.transform/replacerare per-call.parse: AOT macro that parses wire text (from: 'json' | 'query' | 'string') intoResolveDefaults<T>. Input is always astring. Optionalreviverandtransform.TransformFn/TransformContext: Typed per-node rewrite onparse/stringify/assert/validate(ctx.type,ctx.path,ctx.tags). Not used byis/assertGuard.JsonReviver/JsonReplacer:JSON.parse/JSON.stringifycallbacks. Reviver also walks decoded query objects. Replacer is JSON stringify only.tag: Named metadata (tag<'html'>) peeled intoctx.tagsand JSON Schemax-tags.tag.Defaultfills missing properties.jsonSchema: Generates and returns a JSON Schema representation matching a TypeScript type at compile time (draft-07 shaped, withx-typescript-typefor Date/RegExp/Set/Map/bigint/etc. andx-tagsfortag<'html'>).validateSchema/isSchema/assertSchema/assertGuardSchema: Same entrypoints against a runtime JSON Schema value instead of a TypeScript generic.WithModifiers: A utility type that applies constraint, format, or transformation tags to properties of deeply nested or external types using dot-separated path mappings.ResolveDefaults: A helper type that removes the optional flag (?) from properties that have defined default values.convertPropertyCasing: A runtime utility to recursively change the casing of object keys.toZodIssues/groupErrorsByPath: Transform or group validation errors (including nested unionissues).ZodLikeError: Error class wrapping validation errors in a structure compatible with libraries expecting Zod errors.@webergency-utils/typechecker/transformer: Requiredts-patchtransform entry for AOT rewrite.@webergency-utils/typechecker/plugin: Optional language-service plugin for IDE static constraint diagnostics.
API Reference
Validation Functions
validate<T>(input: unknown, options?: ValidationMode | ValidationOptions): IValidation<ResolveDefaults<T>>
Validates input data against type T and returns a structured validation result.
- Parameters:
input: The value to validate.options(optional): Either aValidationModestring ('strict' | 'relaxed' | 'strip') or aValidationOptionsobject (transformis opt-in; not used byis/assertGuard).
- Returns:
IValidation<ResolveDefaults<T>>containing validation status, converted/stripped data, and error details. - Example:
const result = validate<User>(data, 'strip');
is<T>(input: unknown, options?: ValidationMode | GuardOptions): input is ResolveDefaults<T>
A type guard for type T. Always mutates in place (no mutate option). from may coerce nested fields onto the same object; if validation would replace the root value (e.g. primitive "42" → 42), returns false.
- Parameters:
input: The value to check.options(optional): Either aValidationModestring or aGuardOptionsobject.
- Returns:
boolean(trueif valid,falseotherwise). Narrows type ofinputtoResolveDefaults<T>on success. - Example:
if (is<User>(data)) { console.log(data.name); }
assert<T>(input: unknown, options?: ValidationMode | AssertOptions): ResolveDefaults<T>
Validates input data and returns it, throwing a validation error on failure.
- Parameters:
input: The value to validate.options(optional): Either aValidationModestring or anAssertOptionsobject (transformis opt-in; not used byis/assertGuard).
- Returns:
ResolveDefaults<T>(the validated value with defaults resolved). - Throws:
Errorcontaining a list of path and constraint failures, or a custom error viaoptions.errorFactory. - Example:
const user = assert<User>(data);
assertGuard<T>(input: unknown, options?: ValidationMode | AssertGuardOptions): asserts input is ResolveDefaults<T>
An assertion guard for type T. Always mutates in place (no mutate option). from may coerce nested fields; root replacement fails with a normal type error (re-checked without from).
- Parameters:
input: The value to check.options(optional): Either aValidationModestring or anAssertGuardOptionsobject.
- Returns:
void. Narrows the type ofinputin the enclosing scope on success. - Throws:
Errorif validation fails (or a custom error viaoptions.errorFactory). - Example:
assertGuard<User>(data);
jsonSchema<T>(): JsonSchema
Generates a raw JSON Schema draft-07 object matching type T at compile time.
- Returns:
JsonSchema(a JSON Schema object). - Example:
const userSchema = jsonSchema<User>();
validateSchema<T = unknown>(schema: JsonSchema, input: unknown, options?: ValidationMode | ValidationOptions): IValidation<T>
Validates input against a runtime JSON Schema value (not a TypeScript generic).
Only the documented schema subset is compiled. Supported keywords include core types and constraints,
allOf / anyOf / oneOf / not / enum / if/then/else, local $ref / $anchor /
$dynamicRef / $dynamicAnchor / $recursiveRef / $recursiveAnchor, const, type arrays,
patternProperties, propertyNames, dependencies / dependentRequired / dependentSchemas,
contains / minContains / maxContains, prefixItems / additionalItems,
unevaluatedProperties / unevaluatedItems (draft 2019-09 / 2020-12 annotation collection across
in-place applicators including sibling allOf arms), minProperties / maxProperties,
contentEncoding / contentMediaType / contentSchema, and draft-04 boolean
exclusiveMinimum / exclusiveMaximum. Remote http(s) $ref values are rejected.
Schema pattern values are rejected when they exceed the safety limit or contain common catastrophic-backtracking constructs.
Arrays are not valid schemas.
- Parameters:
schema: JSON Schema object.input: The value to validate.options(optional): Same asvalidate.
- Example:
const result = validateSchema({ type: 'string', minLength: 2 }, name);
isSchema(schema: JsonSchema, input: unknown, options?: ValidationMode | GuardOptions): boolean
Schema type-predicate. Always mutates in place; root replacement fails the guard.
assertSchema<T = unknown>(schema: JsonSchema, input: unknown, options?: ValidationMode | AssertOptions): T
Like assert, but against a JSON Schema value.
assertGuardSchema(schema: JsonSchema, input: unknown, options?: ValidationMode | AssertGuardOptions): void
Like assertGuard, but against a JSON Schema value. Root replacement throws.
Serialize / Parse Macros
These are AOT macros rewritten by the transformer into hoisted __ser_* / __parse_* functions that call @webergency-utils/typechecker/runtime.
serializer<T>(options?: ValidationMode | SerializerOptions): (input: T) => string
Compiles a reusable serializer for T.
- Options:
mode:'strict' | 'relaxed' | 'strip'(default'strip') — extra property handling when no index signature is present.format/to:'json'(default) or'query'.transform:TransformFn | TransformFn[]— typed per-leaf rewrite before encode (ctx.type,ctx.path,ctx.tags). Closed over at create time.replacer: JSON.stringify-style replacer (JSON format only). Runs after encode, soDates are ISO strings — offset dates intransform, not here.
- Encodings:
Date→ ISO-8601;Buffer/Uint8Array→ base64;bigint→ decimal digits; tuples fixed-length; tagged unions by discriminant;Record/index signatures serialize extra keys with the value type. - Example:
const serializeUser = serializer<User>({ mode: 'strict' }); const json = serializeUser(user); const dump = serializer<User>({ transform: (value, ctx) => (ctx.type === 'string' ? String(value) + '!' : value), });
stringify<T>(input: T, options?: ValidationMode | SerializerOptions): string
Same codegen as serializer, invoked immediately on input. transform / replacer are per-call.
- Example:
const json = stringify<User>(user); const qs = stringify<Search>(search, { format: 'query' });
parse<T>(input: string, options?: ValidationMode | ParseOptions): ResolveDefaults<T>
Single-pass parse + validate into T. Every parse entry requires a string. Already-parsed JSON objects, query bags, and URLSearchParams are not accepted — use assert / validate for those.
- Options:
mode:'strict' | 'relaxed' | 'strip'(default'strip').from:'json'(default) — JSON text only. AlwaysJSON.parse; unquoted scalars ('hello') are Invalid JSON ('"hello"'is a JSON string). Revives Date/RegExp/etc. after decode.'query'— query/form text that never starts with?(url.search.slice(1), form body). AlwaysparseQueryString;'42'is a flag-key{ 42: true }, not a number. Coerces numbers/booleans/dates. Scalars belong on'string'.'string'— a single already-decoded scalar (path/header/cookie values). Same coercions as'query', but never runsJSON.parse/parseQueryString. Reviver is ignored. Only basic scalar types (string,number,boolean,bigint,Date,RegExp, literals, enums, and unions of these).
reviver: same contract asJSON.parse(bottom-up, rootkey === '',undefineddeletes). Runs on decoded JSON values and decoded query objects. Ignored forfrom: 'string'.transform:TransformFn | TransformFn[]— typed rewrite after revival /transform.*tags (ctx.type,ctx.path,ctx.tagsfromtag<'html'>).fn[]pipes left to right. Throws becomeParseErrorwithctx.path. Skipundefined/nullsotag.Defaultstill fills.
- Behavior: Applies
tag.Default,transform.*, andconstraint.*/format.*(parity withvalidate). RejectsNaN; JSON numbers that surviveJSON.parsemay be±Infinity, butJSON.stringify(Infinity)isnullso Infinity cannot round-trip through JSON text. Query/string numbers must be finite. ThrowsParseError. - Example:
import { parse, stringify, type TransformContext } from '@webergency-utils/typechecker'; const user = parse<User>(req.body); // raw body string, not a parsed object const q = parse<Search>(req.url.search.slice(1), { from: 'query', mode: 'strict' }); const id = parse<string>('jpUllytbmQ=', { from: 'string' }); // stays the string (no querystring parse) const n = parse<number>('42', { from: 'string' }); // → 42 const shift = (delta: number) => (value: unknown, ctx: TransformContext) => { if (ctx.type === 'Date' && value instanceof Date) { return new Date(value.getTime() + delta); } return value; }; const event = parse<Event>(json, { transform: shift(+ms) }); const out = stringify<Event>(event, { transform: shift(-ms) });
Errors
SerializationError— path + message for serialize failures.ParseError— path + message for parse failures.
Utility Functions and Classes
convertPropertyCasing<T, C extends CasingFormat>(obj: T, casing: C, options?: ConvertCasingOptions): ConvertPropertyCasing<T, C>
Recursively converts all property keys of an object to the specified casing format.
If two source keys normalize to the same output key, conversion throws instead of silently discarding a value.
Special keys such as __proto__ are preserved as own data properties without changing the result prototype.
- Parameters:
obj: The source object.casing: ACasingFormatstring value ('snake_case' | 'SNAKE_CASE' | 'camelCase' | 'camelCaseID' | 'PascalCase' | 'PascalCaseID' | 'kebab-case' | 'dot.case').options(optional):ConvertCasingOptionsobject.
- Returns: The casing-converted object with updated TypeScript property keys.
- Example:
const apiResponse = convertPropertyCasing(user, 'camelCase');
toZodIssues(errors: IValidationError[]): any[]
Converts internal validation errors into Zod-compatible issues. Flattens nested union issues into a flat list.
- Parameters:
errors: Array ofIValidationError.
- Returns: An array of Zod-like issues.
groupErrorsByPath(errors: IValidationError[]): Record<string, { value: any, errors: string[] }>
Groups validation errors by path, including nested issues from failed unions.
- Parameters:
errors: Array ofIValidationError.
- Returns: A map of path →
{ value, errors }.
coerceQueryNumber(v: any): any / coerceQueryBoolean(v: any): any / coerceQueryDate(v: any): any / coerceJsonDate(v: any): any
Shared coercion helpers used by from: 'query' / from: 'string' / from: 'json' and by transform.ToNumber / ToBoolean / ToDate.
class ZodLikeError extends Error
An error class wrapper that transforms internal validation errors into a Zod-like error structure.
- Constructor:
constructor(errors: IValidationError[]) - Properties:
name:'ZodError'issues: Zod-like issue array.
Interfaces and Types
type ValidationMode
Controls unknown object keys only. It does not coerce or revive values — that is exclusively from.
| Value | Behavior |
| :--- | :--- |
| 'strict' (default) | Reject properties not declared on the type/schema. |
| 'relaxed' | Allow unknown properties and keep them on the result. No type conversion. |
| 'strip' | Drop unknown properties from the result (in place when mutating). |
// OK: extra keys kept; age stays a string unless you also set from
validate<User>(data, 'relaxed');
// Coercion is a separate axis:
validate<User>(data, { mode: 'relaxed', from: 'query' });interface GuardOptions
Options for is / isSchema. Always mutate in place (no mutate / errorFactory).
| Property | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| mode | ValidationMode | 'strict' | Unknown-key policy (strict / relaxed / strip). Not coercion — see ValidationMode above. |
| from | 'json' \| 'query' \| 'string' \| ((val, ctx) => any) | undefined | In-place coercion for nested fields. 'string' uses the same scalar coercions as 'query' (no querystring parsing). Custom callbacks receive (val, PathContext & { kind: CoercionKind }). Root replacement fails the guard. |
interface AssertGuardOptions
Extends GuardOptions for assertGuard / assertGuardSchema.
| Property | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| errorFactory | (errors: IValidationError[]) => Error | undefined | Custom error factory when the guard throws. |
interface ValidationOptions
Extends GuardOptions for validate / validateSchema (adds mutate; no errorFactory).
| Property | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| mode | ValidationMode | 'strict' | Unknown-key policy (strict / relaxed / strip). Not coercion — see ValidationMode above. |
| from | 'json' \| 'query' \| 'string' \| ((val, ctx) => any) | undefined | Input conversion mode. 'json' revives JSON-impossible types. 'query' also coerces querystring shapes (and may parseQueryString). 'string' coerces a single already-decoded scalar like 'query' but never parses a querystring. A custom function is (val, { key, path, parent, root, index?, kind }) => any and runs only on type mismatch. kind is a CoercionKind dispatch tag (not typeof / a TS type). key is the nearest named path segment (for [n] leaves, the closest named key above). |
| mutate | boolean | false | true: write in place while validating (half-changed input on failure is allowed; union arms still use a side tree). false: always allocate new containers. |
| transform | TransformFn \| TransformFn[] | undefined | Opt-in typed rewrite after revival / transform.* tags. Same contract as parse. Not copied onto is / assertGuard. |
interface AssertOptions
Extends ValidationOptions for assert / assertSchema.
| Property | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| errorFactory | (errors: IValidationError[]) => Error | undefined | Custom error factory when assert throws. |
type CoercionKind / interface PathContext / type FromCoercionContext
CoercionKind: expected runtime kind for customfrom('Date' \| 'Array' \| …) — a dispatch tag, nottypeof.PathContext:{ key, path, parent, root, index? }shared byconstraint.Customand customfrom.FromCoercionContext:PathContext & { kind: CoercionKind }.
interface ParseOptions
Compile-time mode / from stay object literals (distinct __parse_* hoists). reviver / transform are runtime and forwarded into the hoisted parser.
| Property | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| mode | ValidationMode | 'strip' | Unknown-key policy. |
| from | ParseSource ('json' \| 'query' \| 'string') | 'json' | Wire decoder. See parse. |
| reviver | JsonReviver | undefined | After JSON.parse / parseQueryString. Ignored for 'string'. |
| transform | TransformFn \| TransformFn[] | undefined | Typed rewrite after revival / transform.* tags. |
interface SerializerOptions
Compile-time mode / format stay object literals. transform / replacer are runtime. serializer<T>(options) closes over them; stringify takes them per call.
| Property | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| mode | SerializationMode | 'strip' | Extra-property handling when no index signature is present. |
| format / to | SerializeFormat ('json' \| 'query') | 'json' | Output encoding. |
| transform | TransformFn \| TransformFn[] | undefined | Per-leaf rewrite before encode (Dates are still Dates). |
| replacer | JsonReplacer | undefined | JSON only. Runs after encode (JSON.stringify(JSON.parse(out), replacer)), so Dates are ISO strings. |
type TransformFn / interface TransformContext
type TransformFn = (value: unknown, ctx: TransformContext) => unknown;
interface TransformContext {
key: string;
path: string;
parent: any;
root: any;
index?: number;
tags: string[]; // from tag<'html'>, else []
type: CoercionKind; // 'Date' | 'string' | … ; unions / any are best-effort
}Returned value replaces the node. Returning undefined sets undefined (does not mean “skip”). Identity is return value. fn[] pipes left to right. Throws become ParseError / SerializationError with ctx.path (or '' at root). is / assertGuard never apply it.
type JsonReviver / type JsonReplacer
Same contracts as JSON.parse / JSON.stringify: (this: any, key: string, value: any) => any. Bottom-up, root key === '', this is the parent. Reviver undefined deletes the property (JSON spec). Replacer undefined omits the property. Query stringify has no replacer.
interface IValidation<T>
Result object returned by validate.
success:booleandata?:T— present only whensuccessistrueerrors?:IValidationError[]
interface IValidationError
Details of a validation check failure.
path:stringvalue:anyerror:string(the constraint description or custom message)issues?:IValidationError[]— nested failures (used for unions: one summary error with per-arm details)
type WithModifiers<T, M>
Applies constraint, format, or transformation tags to properties of type T using a path mapping M.
- Generics:
T: The baseline type to wrap.M: A key-value map where keys are dot-separated paths (e.g.,'profile.email') and values are tags.
Tags & Modifiers
constraint Namespace
Used to apply value constraints to types.
| Tag | Description |
| :--- | :--- |
| constraint.MinLength<N, Msg?> | Restricts string length to $\ge N$. |
| constraint.MaxLength<N, Msg?> | Restricts string length to $\le N$. |
| constraint.Length<Min, Max> | Shorthand for MinLength<Min> & MaxLength<Max>. |
| constraint.Pattern<Regex, Msg?> | Validates string using regular expression Regex. |
| constraint.Minimum<N, Msg?> | Inclusive minimum value restriction for number | bigint. |
| constraint.Maximum<N, Msg?> | Inclusive maximum value restriction for number | bigint. |
| constraint.Range<Min, Max> | Shorthand for Minimum<Min> & Maximum<Max>. |
| constraint.ExclusiveMinimum<N, Msg?> | Exclusive minimum value restriction for number | bigint. |
| constraint.ExclusiveMaximum<N, Msg?> | Exclusive maximum value restriction for number | bigint. |
| constraint.MultipleOf<N, Msg?> | Restricts number | bigint to multiples of N. |
| constraint.MinItems<N, Msg?> | Restricts array length to $\ge N$. |
| constraint.MaxItems<N, Msg?> | Restricts array length to $\le N$. |
| constraint.UniqueItems<Msg?> | Restricts arrays to deeply unique items. |
| constraint.MinProperties<N, Msg?> | Restricts object own-key count to $\ge N$. |
| constraint.MaxProperties<N, Msg?> | Restricts object own-key count to $\le N$. |
| constraint.PropertiesRange<Min, Max> | Shorthand for MinProperties<Min> & MaxProperties<Max>. |
| constraint.Contains<T, Msg?> | Array must contain ≥1 element matching nested type T (JSON Schema contains). Pair with MinContains / MaxContains for counts. |
| constraint.MinContains<N, Msg?> | Minimum number of contains matches (requires Contains). |
| constraint.MaxContains<N, Msg?> | Maximum number of contains matches (requires Contains). |
| constraint.PropertyNames<T, Msg?> | Every own key must satisfy nested type T (JSON Schema propertyNames). |
| constraint.Custom<Fn, Msg?> | Runs a custom validation function: (val, PathContext) => boolean (key, path, parent, root, optional index). |
| constraint.Requires<Path | [Paths], Msg?> | Enforces that other object property paths exist. |
| constraint.Message<Msg> | Fallback custom error message. |
Object / array cardinality cookbook
MinProperties / MaxProperties / PropertiesRange count own enumerable keys (not array length — use MinItems / MaxItems for that). Contains probes array elements without requiring every element to match. PropertyNames validates each key string.
import { assert, jsonSchema, constraint } from '@webergency-utils/typechecker';
// Own-key count on open objects
type Bag = Record<string, number> & constraint.PropertiesRange<2, 5>;
assert<Bag>({ a: 1, b: 2 });
// assert<Bag>({ a: 1 }); // → MinProperties<2>
// assert<Bag>({ a:1,b:2,c:3,d:4,e:5,f:6 }); // → MaxProperties<5>
// At least two string elements with length ≥ 2 (other elements may be numbers)
type Tags = (string | number)[] &
constraint.Contains<string & constraint.MinLength<2>> &
constraint.MinContains<2> &
constraint.MaxContains<4>;
assert<Tags>(['ab', 1, 'cd']);
// assert<Tags>(['ab', 1]); // → Contains<min:2>
// assert<Tags>(['ab', 'cd', 'ef', 'gh', 'ij']); // → Contains<max:4>
// Every own key must be lowercase letters
type Named = Record<string, number> &
constraint.PropertyNames<string & constraint.Pattern<'^[a-z]+$'>>;
assert<Named>({ aa: 1, bb: 2 });
// assert<Named>({ AA: 1 }); // key fails Pattern
// Optional custom messages (second type argument)
type NeedTwo = Record<string, unknown> &
constraint.MinProperties<2, 'need at least two fields'>;Tag → JSON Schema
jsonSchema<T>() writes the matching draft keywords onto the peeled base schema:
| TypeScript tag | JSON Schema keyword(s) |
| :--- | :--- |
| MinLength / MaxLength / Length | minLength / maxLength |
| Minimum / Maximum / Range | minimum / maximum |
| ExclusiveMinimum / ExclusiveMaximum | exclusiveMinimum / exclusiveMaximum |
| MultipleOf | multipleOf |
| Pattern | pattern |
| Format | format |
| MinItems / MaxItems | minItems / maxItems |
| UniqueItems | uniqueItems: true |
| MinProperties / MaxProperties / PropertiesRange | minProperties / maxProperties |
| Contains<T> | contains ← nested jsonSchema of T |
| MinContains / MaxContains | minContains / maxContains |
| PropertyNames<T> | propertyNames ← nested jsonSchema of T |
| Requires | requires (library extension) |
| tag.Default | default |
| tag<'html'> / tag<'html' \| 'basic'> | x-tags (sorted unique names) |
type Bag = Record<string, number> & constraint.PropertiesRange<2, 5>;
const schema = jsonSchema<Bag>();
// → { type: 'object', additionalProperties: { type: 'number' },
// minProperties: 2, maxProperties: 5 }format Namespace
Standard formats for string primitives.
format.Email: Practical mailbox check (local@domain, length limits, DNS-like domain with a real TLD). Not full RFC 5322.format.IdnEmail: Like email, but Unicode local/domain labels allowed.format.UUID: UUID (v1-v8, plus the nil UUID).format.URL: HTTP/HTTPS/FTP URLs.format.IPv4/format.IPv6: IP addresses.format.Date:YYYY-MM-DDvalidated vianew Date(...)(calendar overflow like2024-02-31is allowed). Withfrom: 'query', the runtime value becomes aDate(the TypeScript type remainsstring & format.Date).format.DateTime: date-time validated vianew Date(...). Withfrom: 'query', returns aDate; otherwise keeps the string.format.ObjectId: MongoDB 24-character hex ObjectId.format.Duration: ISO-8601 duration.format.Time: Time stringHH:MM:SSin real ranges (00-23,00-59,00-59) with an optional fraction and a required timezone (Zor±HH:MM), e.g.19:55:00Z.format.Byte: Base64 string.format.Password: Any string (always valid placeholder).format.Regex: Valid regular expression string.format.Hostname: ASCII domain name (includeslocalhost).format.IdnHostname: Internationalized hostname (Unicode labels).format.URI: Absolute URI.format.UriReference: Absolute URI or relative reference.format.IRI: Absolute IRI (Unicode URI).format.IriReference: Absolute IRI or relative reference.format.UriTemplate: RFC 6570 URI template.- Unknown
Format<'...'>strings fail validation at runtime.
Object and record shapes accept record-like values (plain objects, null-prototype objects, process.env, class instances used as bags). Exotics (Date, Map, Set, arrays, typed arrays, Buffer, …) are rejected unless the type is a dedicated instance type. A real class used as a type is checked with instanceof (nominal); interfaces and type literals stay structural.
transform Namespace
Sanitizes and converts input values during validation.
transform.Trim: Trims string whitespace.transform.LowerCase: Converts string to lowercase.transform.UpperCase: Converts string to uppercase.transform.Capitalize: Capitalizes the first letter.transform.ToNumber: Same coercion asfrom: 'query'for numbers. The entire trimmed string must be a finite decimal/scientific number; partial strings, hexadecimal syntax,NaN, and infinities are rejected.transform.ToBoolean: Same coercion asfrom: 'query'for booleans (true/false/1/0/yes/no/on/off); unknown values are left unchanged and fail the boolean check.transform.ToDate: Same coercion asfrom: 'query'for dates (parseable strings and finite timestamps).transform.Custom<Fn>: Custom mapping function:(val) => any.
tag Namespace
tag.Default<Value>: InjectsValuewhen a property is undefined. Removes the optional modifier (?) when resolved withResolveDefaults<T>.tag<'html'>/tag<'html' | 'basic'>(andtag<'html'> & tag<'basic'>): named metadata on an optional__tagsbag. Peeled at compile time; no handler class or global registry. Runtime behaviour is thetransformoption (ctx.tags, sorted).jsonSchema<T>()emitsx-tags.__tagsis reserved like__default.
import { parse, tag } from '@webergency-utils/typechecker';
interface Article {
title: string;
body: string & tag<'html' | 'basic'>;
}
parse<Article>(json, {
transform: (value, ctx) => {
// ctx.tags: title → [] ; body → ['basic', 'html']
return value;
},
});Troubleshooting
validate, is, assert, parse, or stringify throw “transformer was not applied”
- Cause: The compiler transformer did not rewrite the call at build time. Untransformed stubs always throw.
- Diagnostics Check: Inspect your built
.jsoutput. If it still containsvalidate(...)/is(...)/assert(...)as package imports rather than__tcRuntime.validate(...)(etc.), the transformer did not run. - Fix:
- Verify
npx ts-patch installwas executed successfully. - Verify
{ "transform": "@webergency-utils/typechecker/transformer" }is registered intsconfig.jsoncompilerOptions.plugins. - Ensure your bundler or compiler CLI compiles using patched
tsc.
- Verify
IDE does not report constraint errors on literals
- Cause: The optional language service plugin is not loaded.
- Fix: Add
{ "name": "@webergency-utils/typechecker/plugin" }alongside the transformer entry intsconfig.jsonplugins, and restart the TypeScript language service in your editor.
Maintenance
This package is actively maintained.
Bug reports and pull requests are welcome. Security issues and critical regressions are prioritized. New features are considered when they align with the package's existing scope.
