@illavv/run_typer - v4.1.0
    Preparing search index...

    @illavv/run_typer - v4.1.0

    Typer - Advanced TypeScript Type Validation Library

    Coverage Badge Build Status TypeScript License Version

    Typer is a comprehensive TypeScript validation library that provides robust type checking, schema validation, and runtime type safety. Built with modern TypeScript features including generics, type guards, and advanced type inference.

    • ๐Ÿ” Comprehensive Type System: Support for all JavaScript types including advanced types (BigInt, TypedArrays, etc.)
    • ๐ŸŽฏ Type-checked schemas: Infer<typeof schema> derives the static type from the runtime schema, and a typo like 'nubmer' is a compile error, not a runtime surprise (4.0+)
    • ๐Ÿงฉ Composable validators: literal, arrayOf, record, tuple, refine, transform, withDefault, lazy, objectOf โ€” all nest freely (4.0+)
    • ๐Ÿ”Ž Structured errors: every failure carries code, path, expected and received, so you branch on data instead of parsing strings (4.0+)
    • ๐Ÿค Standard Schema: typer.standard(schema) is accepted by tRPC, Hono, TanStack Form and Router, Nuxt and the rest โ€” no adapter (4.1+)
    • ๐Ÿ“‹ Schema Validation: Complex nested object structure validation with strict mode
    • ๐Ÿ”ง Extensible Architecture: Register custom types with extend() and keep full type inference
    • ๐Ÿชถ Small and instant: under 10 KB gzip, and building a schema costs ~1.1 ยตs โ€” the lowest setup cost of the three libraries measured. See Performance for where it wins and where it does not
    • ๐Ÿ“ JSON Schema output: toJSONSchema() for OpenAPI and Swagger tooling (4.1+)
    • ๐Ÿ›ก๏ธ Runtime Safety: Catch type errors at runtime with detailed error messages
    • ๐Ÿ“ฑ Phone Number Validation: International phone number validation (ITU-T E.164 standard)
    • ๐Ÿ“ง Advanced Validations: Email, URL, UUID, IP, semver, slug, JWT, MAC and more
    npm install @illavv/run_typer
    
    import { Typer } from '@illavv/run_typer';

    const typer = new Typer();

    // 3.1+: pass a literal alias and the return type is inferred โ€” no <generic> needed
    const message = typer.isType('string', 'Hello World'); // message: string
    const count = typer.isType('number', 42); // count: number

    // Type guards narrow automatically from the literal alias
    if (typer.is(userInput, 'string')) {
    // userInput is now typed as string
    console.log(userInput.toUpperCase());
    }

    // Non-throwing variant
    const result = typer.safeParse('number', userInput);
    if (result.success) {
    // result.data: number
    }
    import { Typer, type Infer } from '@illavv/run_typer';

    const typer = new Typer();

    // One literal drives both runtime validation and the static type
    const user = typer.parse(
    { id: 'number', name: 'string', email: 'string?' },
    payload,
    );
    // user is typed: { id: number; name: string; email?: string | null }

    // Reusable schemas with derived types โ€” no `as const` needed
    const userSchema = typer.schema({
    id: 'number',
    name: 'string',
    email: 'string?',
    tags: ['string'],
    address: { city: 'string', zip: 'string?' },
    });
    type User = Infer<typeof userSchema>;

    const u = typer.parse(userSchema, payload); // throws + typed as User
    const r = typer.safeParse(userSchema, payload); // { success, data: User } | { error }

    Mix-and-match validator functions inside a schema โ€” any slot accepts a Validator<T>, whether from a built-in helper, a combinator, or your own:

    const strict = typer.schema({
    id: typer.validators.isPositiveInteger,
    name: typer.validators.isNonEmptyString,
    color: (v) => typer.isHexColor(v),
    role: typer.literal('admin', 'user', 'guest'),
    });

    Use typer.validators.isX, not typer.isX, when passing a validator as a value. A bare method reference loses its this and fails even on valid input. typer.validators holds the same validators, pre-bound; wrapping in an arrow โ€” (v) => typer.isX(v) โ€” works just as well.

    Note: a type string like 'admin|user|guest' means "one of these type aliases", not "one of these values". Use typer.literal(...) for specific values.

    A misspelled alias used to infer unknown and blow up at runtime. Now it does not compile:

    typer.parse({ id: 'nubmer' }, payload);
    // ~~~~~~~~
    // Type '"nubmer"' is not assignable to type
    // '`Typer: unknown type alias in "nubmer" โ€” register it with extend() or fix the spelling`'

    This covers unions, optionals, array elements and nested schemas:

    typer.schema({ a: 'number|strng' });   // โŒ
    typer.schema({ b: 'nubmer?' }); // โŒ
    typer.schema({ c: ['strng'] }); // โŒ
    typer.schema({ d: { e: 'strng' } }); // โŒ

    Dynamically built schemas carry no literal information to check, so they are still accepted:

    const dynamic: Record<string, string> = buildSchema();
    typer.checkStructure(dynamic, payload); // fine

    Every failure carries machine-readable issues, so you branch on data rather than parsing messages:

    const result = typer.safeParse(
    { id: 'number', address: { city: 'string' } },
    { id: 'nope', address: { city: 1 } },
    );

    if (!result.success) {
    result.issues;
    // [
    // { code: 'invalid_type', path: 'id', expected: 'number', received: 'string', message: 'โ€ฆ' },
    // { code: 'invalid_type', path: 'address.city', expected: 'string', received: 'number', message: 'โ€ฆ' },
    // ]
    }

    Paths use dotted and indexed notation (address.city, tags[2]). code is one of:

    Code Meaning Extra fields
    invalid_type present, but not one of the expected types expected, received
    missing_key a required key was absent expected
    unknown_type the schema named an alias that is not registered expected
    invalid_schema the schema itself is malformed expected, received
    unexpected_key strict mode: a key the schema does not declare
    too_small below a lower bound โ€” too short, too few, too small (4.1+) minimum
    too_big above an upper bound โ€” too long, too many, too large (4.1+) maximum
    invalid_format right type, wrong shape: not an email, not a UUID, not an integer (4.1+) expected
    dangerous_key an unsafe key could not be stripped (4.1+)
    custom a validator in the schema threw without reporting a reason

    Constraint validators carry their code all the way out, whether called directly or from inside a schema โ€” which is what makes "too short" tellable from "out of range" without reading the message:

    const result = typer.safeParse({
    name: (v) => typer.isLength({ min: 3, max: 50 }, v),
    pin: (v) => typer.isInRange(1000, 9999, v),
    }, { name: 'ab', pin: 42 });

    result.issues;
    // [
    // { code: 'too_small', path: 'name', minimum: 3, message: 'โ€ฆ' },
    // { code: 'too_small', path: 'pin', minimum: 1000, maximum: 9999, message: 'โ€ฆ' },
    // ]

    parse throws a TyperError, which extends TypeError โ€” existing instanceof TypeError handling keeps working:

    import { TyperError } from '@illavv/run_typer';

    try {
    typer.parse(userSchema, payload);
    } catch (e) {
    if (e instanceof TyperError) {
    e.issues; // the structured list
    e.flatten(); // { 'address.city': ['Expected "address.city" to be string, got number'] }
    }
    }

    result.error is built on first access. Reading only result.issues skips constructing an Error โ€” whose stack capture costs more than the validation itself.

    toJSONSchema() turns a schema into the document OpenAPI and Swagger tooling expects:

    typer.toJSONSchema({ id: 'number', email: typer.validators.isEmail, note: 'string?' });
    // {
    // $schema: 'https://json-schema.org/draft/2020-12/schema',
    // type: 'object',
    // properties: {
    // id: { type: 'number' },
    // email: { type: 'string', format: 'email' },
    // note: { type: ['string', 'null'] },
    // },
    // required: ['id', 'email'],
    // }

    Type strings, ? markers, | unions, arrays and nested objects have exact equivalents. Validators do not โ€” a validator is an opaque function โ€” so Typer's own validators and combinators carry the fragment they correspond to:

    Slot Emitted
    typer.validators.isEmail { type: 'string', format: 'email' }
    typer.literal('a', 'b') { enum: ['a', 'b'] }
    typer.arrayOf(v, { min: 1 }) { type: 'array', items: โ€ฆ, minItems: 1 }
    typer.tuple([a, b]) { type: 'array', prefixItems: [โ€ฆ], minItems: 2, maxItems: 2 }
    typer.record(v) { type: 'object', additionalProperties: โ€ฆ }
    typer.optional(v) the inner fragment, key dropped from required
    typer.withDefault(v, 10) the inner fragment plus default: 10
    typer.discriminatedUnion(k, โ€ฆ) { oneOf: [โ€ฆ], discriminator: { propertyName: k } }

    A validator you wrote becomes {} โ€” which accepts anything โ€” as do the aliases JSON cannot carry (symbol, function, map, set, regexp, the buffer types) and anything registered with extend. Pass { unrepresentable: 'throw' } in a build step to be told about those instead of shipping a schema that quietly accepts anything at those keys:

    typer.toJSONSchema(schema, { unrepresentable: 'throw' });
    // TyperError: Cannot convert to JSON Schema: 1 slot(s) have no equivalent โ€” session

    { strict: true } emits additionalProperties: false, and { $schema: false } omits the dialect keyword for embedding in a larger document.

    Query strings, form data and environment variables arrive as strings. Without coercion, every handler rewrites the conversion by hand โ€” which is where the mistakes are:

    const query = typer.parse({
    page: typer.coerce.number,
    archived: typer.coerce.boolean,
    since: typer.coerce.date,
    }, req.query);
    // { page: 2, archived: false, since: Date }

    The converted value replaces the original in place, so what comes out of parse holds numbers and dates rather than the strings that arrived.

    These reject what they cannot convert instead of inventing a value, which is the whole difference from the Number() / Boolean() they replace:

    Input Number() / Boolean() typer.coerce.*
    '' 0 rejected
    ' ' 0 rejected
    null 0 rejected
    [] 0 rejected
    'abc' NaN rejected
    'false' true false
    '0' true false

    coerce.boolean reads true/1/yes/on and false/0/no/off, in any case and with surrounding whitespace; anything else is rejected rather than guessed at.

    Real applications derive shapes from each other constantly. Writing the second copy by hand means the two diverge at the first change:

    const userSchema = typer.schema({
    id: 'number',
    name: 'string',
    email: 'string',
    password: 'string',
    });

    const publicUser = typer.pick(userSchema, ['id', 'name']);
    const createUser = typer.omit(userSchema, ['id']);
    const patchUser = typer.partial(typer.omit(userSchema, ['id', 'password']));
    const timestamped = typer.merge(userSchema, { createdAt: 'date' });

    type PatchUser = Infer<typeof patchUser>;
    // โ†’ { name?: string | null; email?: string | null }

    All four return a plain new schema and leave the sources untouched, so the result parses, infers and composes like any other. merge lets the second schema win on overlapping keys.

    partial gives a type-string slot the ? marker it already understands. Slots with no marker of their own โ€” a validator, an array, a nested schema โ€” are rebuilt as optional(...) validators instead, so a made-optional nested slot reports its failures as one custom issue at the slot's path rather than one per field. Pass the keys you need if that matters: typer.partial(schema, ['name', 'email']).

    Typer validates in place and returns the same reference it was given. That is what makes it fast, and it used to mean that keys the schema never declared โ€” including __proto__ and constructor from a JSON.parse payload โ€” survived a success: true:

    const payload = JSON.parse('{"id":1,"__proto__":{"admin":true}}');
    typer.parse({ id: 'number' }, payload);

    Object.keys(payload); // ['id'] โ€” was ['id', '__proto__'] before 4.1
    { ...payload }.admin; // undefined โ€” the spread that used to matter

    __proto__, constructor and prototype are now removed from every validated object unless the schema declares them as fields of its own, in which case they are ordinary keys and are validated like any other. Nothing else about extra keys changes: use strict mode if you want all undeclared keys rejected.

    If the object is frozen and the key cannot be removed, validation fails with a dangerous_key issue rather than passing an object it could not make safe.

    The check costs ~2.5 ns per object: a cheap probe that only falls through to the exact hasOwnProperty test when an object is not a plain, unpolluted one.

    Standard Schema is the common contract that lets a validation library be accepted by tRPC, Hono, TanStack Form and Router, Nuxt and dozens of others without a per-library adapter. standard() produces it from a schema, a validator, or a type alias:

    import typer from '@illavv/run_typer';

    const userSchema = typer.standard({ id: 'number', email: 'string' });

    // Any Standard Schema consumer accepts it as-is:
    app.post('/users', validator('json', userSchema), handler); // Hono
    publicProcedure.input(userSchema).mutation(({ input }) => โ€ฆ); // tRPC

    The result is still an ordinary Validator, so it keeps working everywhere a validator does โ€” including nested inside another schema:

    userSchema({ id: 1, email: 'a@b.co' });        // returns the value, throws on failure
    typer.arrayOf(userSchema); // composes like any other validator

    objectOf() carries the contract as well, so an existing typer.objectOf(...) is already a Standard Schema.

    Issue paths follow the spec's segment form, while keeping Typer's machine-readable code:

    userSchema['~standard'].validate({ id: 'one', email: 'a@b.co' });
    // {
    // issues: [{
    // code: 'invalid_type',
    // path: ['id'], // segments, not 'id'
    // message: 'Expected "id" to be number, got string',
    // expected: 'number',
    // received: 'string',
    // }]
    // }

    Validation is synchronous: validate never returns a Promise.

    All of these return a Validator<T>, so they compose with each other and slot into any schema position:

    const orderSchema = typer.schema({
    id: typer.validators.isUUID,
    status: typer.literal('pending', 'shipped', 'cancelled'),
    qty: typer.refine((v) => typer.asNumber(v), (n) => n > 0, 'qty must be > 0'),
    coupon: typer.withDefault((v) => typer.asString(v), ''),
    sizes: typer.arrayOf((v) => typer.asNumber(v), { min: 1, max: 10 }),
    labels: typer.record((v) => typer.asString(v)), // Record<string, string>
    position: typer.tuple([(v) => typer.asNumber(v), (v) => typer.asNumber(v)]), // [number, number]
    placedAt: typer.instanceOf(Date),
    slug: typer.transform((v) => typer.asString(v), (s) => s.trim().toLowerCase()),
    lines: typer.arrayOf(typer.objectOf({ sku: 'string', qty: 'number' })),
    });

    discriminatedUnion covers the payload shape most APIs actually use โ€” a union told apart by one key (4.1+):

    const event = typer.discriminatedUnion('type', {
    created: { id: 'string', at: 'date' },
    renamed: { id: 'string', name: 'string' },
    deleted: { id: 'string' },
    });

    type Event = ReturnType<typeof event>;
    // โ†’ { type: 'created'; id: string; at: Date }
    // | { type: 'renamed'; id: string; name: string }
    // | { type: 'deleted'; id: string }

    Unlike union, which tries each variant in turn, this reads the discriminant once and goes straight to the only variant that can match โ€” in constant time, however many there are โ€” and reports against that variant alone:

    typer.safeParse(event, { type: 'renamed', id: 'x', name: 42 }).issues;
    // [{ code: 'invalid_type', path: 'name', expected: 'string', received: 'number' }]
    // not "none of the 3 variants matched, here is why each one failed"

    Variants are keyed by discriminant value, so the mapping is exact by construction. Each member carries its own tag as a literal type, so it narrows on type the way a hand-written union does.

    lazy makes recursive shapes expressible:

    type Node = { name: string; children?: Node[] };

    const node: Validator<Node> = typer.lazy(() => typer.objectOf({
    name: 'string',
    children: typer.optional(typer.arrayOf(node)),
    }) as Validator<Node>);

    extend() is registerType that also records the alias in the instance's type, so it works in compile-checked schemas and resolves in Infer:

    const typer = new Typer()
    .extend('positive', (v): number => {
    if (typeof v !== 'number' || v <= 0) throw new TypeError('Must be positive');
    return v;
    })
    .extend('slug', (v): string => new Typer().isSlug(v));

    const schema = typer.schema({ qty: 'positive', handle: 'slug' });
    type Product = Infer<typeof schema, { positive: number; slug: string }>;
    // โ†’ { qty: number; handle: string }

    It returns the same instance, just re-typed, so it chains. registerType is still available when you do not need the alias in the type system.

    import { Typer } from '@illavv/run_typer';

    const typer = new Typer();

    // Type guards (return boolean)
    console.log(typer.is<string>("Hello", "string")); // true
    console.log(typer.is<number>(123, "number")); // true
    console.log(typer.is<boolean>(true, "boolean")); // true
    console.log(typer.is<unknown[]>([], "array")); // true
    console.log(typer.is<object>({}, "object")); // true

    // Type validation (throws on error)
    const str = typer.isType<string>('string', 'Hello'); // Returns 'Hello' typed as string
    const num = typer.isType<number>('number', 42); // Returns 42 typed as number
    const typer = new Typer();

    // Multiple type validation
    const value = typer.isType<string | number>(['string', 'number'], 'Hello');

    // Specific validations
    const email = typer.isEmail('user@example.com');
    const phone = typer.isPhoneNumber('+1234567890');
    const url = typer.isURL('https://example.com');

    // Array validations
    const numbers = typer.isArrayOf<number>('number', [1, 2, 3]);
    const nonEmpty = typer.isNonEmptyArray<string>(['a', 'b']);

    // Range and constraints
    const age = typer.isInRange(18, 65, 25);
    const positiveInt = typer.isPositiveInteger(42);
    const typer = new Typer();

    // Modern JavaScript types
    const bigIntVal = typer.isType<bigint>('bigint', BigInt(123));
    const buffer = typer.isType<ArrayBuffer>('arraybuffer', new ArrayBuffer(8));
    const typedArray = typer.isType<Int32Array>('typedarray', new Int32Array(4));
    const dataView = typer.isType<DataView>('dataview', new DataView(buffer));

    // Collections
    const map = typer.isType<Map<string, number>>('map', new Map());
    const set = typer.isType<Set<string>>('set', new Set());

    // Special validations
    const jsonStr = typer.isType<string>('json', '{"valid": "json"}');
    const validDate = typer.isType<Date>('date', new Date());
    const regex = typer.isType<RegExp>('regexp', /pattern/);
    const typer = new Typer();

    const userSchema = {
    name: "string",
    age: "number",
    email: "string?", // Optional field
    address: {
    street: "string",
    city: "string",
    zip: "number|string" // Union types
    },
    hobbies: ["string"], // Array of strings
    isActive: "boolean"
    };

    const userData = {
    name: "John Doe",
    age: 30,
    address: {
    street: "123 Main St",
    city: "New York",
    zip: 10001
    },
    hobbies: ["reading", "coding"],
    isActive: true
    };

    const result = typer.checkStructure(userSchema, userData);
    if (result.isValid) {
    console.log("โœ… Valid user data");
    } else {
    console.log("โŒ Validation errors:", result.errors);
    }

    // Strict mode (rejects extra properties)
    const strictResult = typer.checkStructure(userSchema, userData, '', true);
    const typer = new Typer();

    // Register a custom validator
    typer.registerType("positive", (value) => {
    if (typeof value !== "number" || value <= 0) {
    throw new TypeError("Value must be a positive number");
    }
    return value;
    });

    // Register email validator with override
    typer.registerType("email", (value) => {
    const email = typer.isType<string>('string', value);
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
    throw new TypeError('Invalid email format');
    }
    return email;
    }, true); // Override existing if present

    // Use custom type
    console.log(typer.is(10, "positive")); // true
    console.log(typer.is(-5, "positive")); // false

    // List all types
    console.log(typer.listTypes());

    // Export/Import types
    const typesJson = typer.exportTypes();
    typer.importTypes(typesJson);

    // Remove custom type
    typer.unregisterType("positive");
    const typer = new Typer();

    // Single parameter function
    const safeMultiply = typer.expect(
    (x: number) => x * 2,
    {
    paramTypes: ['number'],
    returnType: ['number']
    }
    );

    console.log(safeMultiply(4)); // 8
    // safeMultiply("hello"); // Throws TypeError

    // Multiple parameters
    const safeAdd = typer.expect(
    (x: number, y: number) => x + y,
    {
    paramTypes: ['number', 'number'],
    returnType: ['number']
    }
    );

    // Async function support
    const asyncFunc = typer.expect(
    async (x: number): Promise<string> => x.toString(),
    {
    paramTypes: ['number'],
    returnType: ['string']
    }
    );

    // Multiple return types
    const flexibleFunc = typer.expect(
    (x: boolean) => x ? 42 : "string",
    {
    paramTypes: ['boolean'],
    returnType: ['number', 'string']
    }
    );

    Type guard that returns boolean. Safe for TypeScript type narrowing.

    Validates type and returns the value cast to T. Throws TypeError on failure.

    Validates and returns a string. Alias for isType<string>('string', value).

    Validates and returns a number.

    Validates and returns a boolean.

    Validates and returns an array.

    Validates and returns an object.

    Validates email format.

    Validates URL format.

    Validates international phone numbers (7-15 digits, ITU-T E.164 standard).

    Validates array with specific element type.

    Validates non-empty strings.

    Validates non-empty arrays.

    Validates value is one of specified options.

    Validates number within range.

    Validates integer values.

    Validates positive numbers.

    Validates positive integers.

    Validates negative numbers.

    Validates negative integers.

    Stricter than isType('number', x): rejects NaN and Infinity. (3.1+)

    Validates integers within Number.MIN/MAX_SAFE_INTEGER. (3.1+)

    Validates UUID strings (RFC 4122, versions 1โ€“5). (3.1+)

    Validates IPv4 / IPv6 addresses. (3.1+)

    Validates CSS hex colors (#RGB, #RGBA, #RRGGBB, #RRGGBBAA). (3.1+)

    Validates an ISO 8601 string and returns the parsed Date. (3.1+)

    Validates Base64 strings. (3.1+)

    Validates a plain object literal (rejects class instances, arrays, Map, Set). (3.1+)

    Validates a Promise / thenable. (3.1+)

    Type-safe instanceof check. (3.1+)

    Validates a string matches the given regex. (3.1+)

    Validates the length of a string or array. (3.1+)

    Polymorphic emptiness check across string, array, Map, Set, object. (3.1+)

    Validates an IP address of either version. (4.0+)

    Validates a Semantic Versioning 2.0.0 string, pre-release and build metadata included. (4.0+)

    Validates a URL-friendly slug (hello-world). (4.0+)

    Validates a TCP/UDP port number (1โ€“65535; 0 is rejected). (4.0+)

    Validates the shape of a JSON Web Token. Does not verify the signature โ€” never use it as an authentication check. (4.0+)

    Validates a MAC address in colon- or hyphen-separated form. (4.0+)

    Wraps a validator so null is also accepted.

    Wraps a validator so undefined is also accepted.

    Tries each validator in order; succeeds on the first match.

    A union told apart by one key. Selects the variant in constant time on the discriminant value and reports against that variant alone. Variants are keyed by discriminant value; each member carries its tag as a literal type.

    Accepts only the listed literal values, narrowing to their union. (4.0+)

    Validates every element, reporting all failures at once. (4.0+)

    Validates a dictionary object's values. Rejects arrays and null. (4.0+)

    Validates a fixed-length, heterogeneous array. (4.0+)

    Adds a constraint without changing the type. (4.0+)

    Maps a validated value. The transformer only ever sees valid input. (4.0+)

    Substitutes a default for undefined (not for null). Pass a factory for object/array defaults so instances are not shared. (4.0+)

    Defers construction, which is what makes recursive shapes expressible. Runs the factory once. (4.0+)

    Composable form of isInstanceOf. (4.0+)

    Turns a schema into a validator, so object shapes nest inside the other combinators. The result also carries ~standard. (4.0+, Standard Schema in 4.1+)

    Wraps a schema, validator or type alias as a Standard Schema, so it is accepted by tRPC, Hono, TanStack Form and Router, Nuxt and others with no adapter. The result stays a callable Validator. (4.1+)

    The is* / as* validators, pre-bound to the instance, so they can be passed as values without losing this. Built on first access and cached. (4.0+)

    coerce.number, coerce.boolean, coerce.date โ€” validators that convert before validating, for query strings, form data and environment variables. They reject what they cannot convert ('', null, [], 'abc') instead of producing 0, and read 'false' as false. (4.1+)

    Same input as parse, returns a discriminated union instead of throwing: { success: true, data } | { success: false, issues, error }.

    issues is a ValidationIssue[] โ€” { code, path, message, expected?, received? }. error is a TyperError (which extends TypeError) built lazily on first access, so reading only issues avoids the cost of capturing a stack trace. (issues added in 4.0)

    Universal entry point. Accepts a type alias, an array of aliases, a Validator<T> function, or a Schema object. Throws TypeError on failure; on success returns the value typed via Infer<S> (for schemas) or TypeMap[K] (for type aliases).

    Non-throwing variant. Returns { success: true, data } | { success: false, error: TypeError }.

    Converts a schema into a JSON Schema document (draft 2020-12 by default), for OpenAPI and Swagger tooling. Options: $schema (dialect, or false to omit), id, title, description, strict (emits additionalProperties: false), and unrepresentable ('any' by default, 'throw' to fail on slots with no JSON Schema equivalent).

    Derives a schema keeping only the listed keys. Returns a new schema; the source is untouched.

    Derives a schema without the listed keys โ€” the complement of pick.

    Combines two schemas. Keys of extension win where the two overlap.

    Makes every key optional, or only the listed ones. Type-string slots gain the ? marker; validator, array and nested-schema slots are rebuilt as optional(...) validators and report failures as a single custom issue at the slot path.

    Identity helper that preserves literal types of a schema declared in a variable. Use it to derive Infer<typeof schema> without as const.

    Derives a TypeScript type from a runtime schema literal โ€” handles ?-suffix optionals, |-unions, array elements, nested objects, and embedded Validator<T> functions.

    Validates object structure against schema. Returns {isValid: boolean, errors: string[]}. Lower-level than parse โ€” kept for backward compatibility and for the strict-mode entry point.

    Registers custom type validator.

    Removes registered type.

    Returns all registered type names.

    Exports types as JSON string.

    Imports types from JSON string.

    Wraps function with type checking for parameters and return value.

    Validates object against simple schema, returns error array.

    Logs warning if type assertion fails.

    Primitives: string, number, boolean, bigint, symbol, undefined, null

    Objects: object, array, function, date, regexp, map, set

    Advanced: arraybuffer, dataview, typedarray, json, domelement

    Aliases: Short forms like s/str for string, n/num for number, etc.

    Validation libraries are usually sold on speed. Typer's honest position is narrower and worth stating plainly, because a library that says where it loses is easier to trust on where it wins.

    From the 4.0.0 audit, against zod 4.6.5 and @sinclair/typebox 0.34.52 on Node 26 / darwin arm64, each library measured in its own equivalent-work lane:

    Scenario Typer Zod TypeBox JIT
    Schema setup 1.13 ยตs 5.74 ยตs 3.43 ยตs ๐Ÿฅ‡
    Bundle (gzip) 7.0 KB 92 KB 22.4 KB ๐Ÿฅˆ *
    Flat object, valid 90 ns 68 ns 3.3 ns ๐Ÿฅ‰
    Nested object, 25 items 1.74 ยตs 807 ns 191 ns ๐Ÿฅ‰
    Flat object, invalid 404 ns 354 ns 1.05 ยตs ๐Ÿฅˆ
    Deep nested, invalid 2.07 ยตs 1.27 ยตs 15.19 ยตs ๐Ÿฅˆ

    * behind zod/mini at 4.8 KB. Those bundle figures are 4.0.0's; 4.1 added Standard Schema, schema composition, coercion, discriminated unions and JSON Schema output, and npm run size now reports 9.7 KB gzip for the full ESM bundle. None of it can be tree-shaken away by a consumer who does not use it, because the API hangs off a class instance โ€” which is what the 5.0 modularization is for.

    On the hot path Typer is slower than Zod, and about nine times slower than a compiled TypeBox. That is not a problem in itself โ€” 90 ns is far below the point where validation is visible inside an HTTP handler โ€” but it is not the reason to choose Typer either. The three real wins are instant setup, a small bundle, and schemas that read like the shape they describe.

    Compiled checkers are cached by schema object identity. A schema literal written inside a handler is a new object on every call, so it is recompiled every time โ€” about an order of magnitude slower, and silent, because the code looks perfectly ordinary:

    const userSchema = typer.schema({ id: 'number' });          // compiled once
    app.post('/u', (req) => typer.parse(userSchema, req.body)); // 78 ns

    app.post('/u', (req) => typer.parse({ id: 'number' }, req.body)); // 873 ns

    It is a cost, not a leak: the cache is a WeakMap, so the throwaway schema is collected normally.

    Schemas are compiled to closures once and cached by schema identity; every type name, optional marker and union alternative is resolved at compile time, so validating is a flat loop over predicates. Error paths are only built when something actually fails.

    npm run bench on Node 26, median of 7 batches (absolute numbers are machine-dependent):

    Operation ops/sec
    is(value, 'string') ~240M
    isType('string', value) ~165M
    isArrayOf('number', 50 items) ~45M
    parse(flat schema) โ€” valid ~24M
    parse(nested schema) โ€” valid ~8M
    checkStructure(nested) ~7M
    safeParse(nested) โ€” invalid ~2M

    The last row is the one that changed most in 4.0 (17ร— faster): a failing validation no longer throws, catches and rebuilds its own error internally.

    The is/isType rows measure a few-nanosecond operation and swing widely with V8's inlining decisions โ€” treat a change there as noise unless it reproduces across runs. The schema rows are stable to within a few percent.

    Typer has 97% line / 99.4% function / 96.9% statement / 91.9% branch coverage with 406 tests covering:

    • All type validators and edge cases
    • Schema validation scenarios
    • Custom type registration and built-in overrides
    • Structured error reporting
    • Combinators and composition
    • Function wrapping and validation
    • TypeScript generic integration
    npm test               # Jest suite + type-level suite
    npm run test:watch # Watch mode
    npm run test:coverage # Coverage report
    npm run test:types # Type-level assertions only
    npm run bench # Benchmarks

    The type-level suite (tests/types/) is checked by tsc, not Jest: it asserts that Infer produces exactly the expected types, and that invalid schemas are rejected. It is part of npm test.

    The uncovered lines are the return p statements of the built-in checkers. Since the predicate fast-path landed, those checkers only ever run to throw โ€” see the TODO(tech-debt) note in src/Typer.ts.

    npm run build          # Build all formats
    npm run build:docs # Build with documentation

    Outputs:

    • dist/Typer.min.js - UMD format
    • dist/Typer.esm.mjs - ES modules
    • dist/Typer.cjs.min.js - CommonJS
    • dist/Typer.d.ts - TypeScript definitions

    MIT License - see LICENSE file for details.

    Contributions are welcome! Please:

    1. Fork the repository
    2. Create a feature branch
    3. Add tests for new functionality
    4. Ensure all tests pass
    5. Submit a pull request

    Michael Lavigna


    Built with โค๏ธ and TypeScript