npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

valchecker

v0.0.33

Published

Runtime-first TypeScript validation with a tree-shakeable fluent API.

Readme

Valchecker

A modular TypeScript validation library with composable immutable steps, full transformed-output inference, structured issues, and a tree-shakable fluent API.

Installation

pnpm add valchecker
# or
npm install valchecker

Quick start

import { v } from 'valchecker'

const userSchema = v.object({
	name: v.string().toTrimmed().isNotEmpty(),
	age: v.number().isFinite().isInteger().isAtLeast(0),
	tags: v.array(v.string()).isLengthAtMost(10),
})

const result = userSchema.execute({
	name: '  Alice  ',
	age: 30,
	tags: ['typescript'],
})

if (v.isSuccess(result)) {
	console.log(result.value)
}
else {
	console.error(result.issues)
}

Selective imports

import {
	createValchecker,
	isAtLeast,
	isFinite,
	number,
} from 'valchecker'

const v = createValchecker({
	steps: [number, isFinite, isAtLeast],
})

const schema = v.number().isFinite().isAtLeast(0)

Use allSteps when a custom instance should include every built-in plugin.

Map and Set collections

const tags = v.set(v.string().toTrimmed().toLowercase())
	.isNotEmpty()
	.isSizeAtMost(5)
	.isIncluding('required')

const scoreCount = v.map({
	key: v.string().toTrimmed(),
	value: v.number().isFinite(),
})
	.isIncludingKey('primary')
	.isIncludingValue(1)
	.toSize()

Both initial schemas preserve insertion order, return new transformed collections, and expose stable child paths. Duplicate transformed Set items or Map keys are validation failures rather than silent data loss.

Map and Set schemas expose size-aware emptiness checks, isSizeAtLeast(), isSizeAtMost(), isSizeExactly(), and toSize(). Set membership uses isIncluding(), while Map membership is explicit through isIncludingKey() and isIncludingValue().

Use toArray() for Set items and toKeys(), toValues(), or toEntries() for explicit Map representations. Each transform returns a new insertion-ordered array and emits no new issue.

Set schemas expose toMapped() and toFiltered(). Map schemas expose explicit toMappedKeys() and toMappedValues() transforms. These callbacks traverse a step-start snapshot synchronously; mapped Set items and Map keys must remain unique under SameValueZero.

Records and tuples

const ratings = v.record({ key: v.string(), value: v.number() }) // { [k: string]: number }
const flags = v.record({ key: v.union(['read', 'write']), value: v.boolean() }) // { read: boolean, write: boolean }

const point = v.tuple([v.string(), v.number(), '...', v.array(v.number())]) // [string, number, ...number[]]

record() validates every own enumerable entry as a plain object Record<K, V>. Finite literal-union keys produce an all-required, exhaustive object; wide keys produce an open index signature and must transform uniquely. tuple() validates a fixed-shape array positionally, with a single optional '...' rest region whose array output is spread. The object optional-field [schema] shorthand never collides with tuple().

Direct variant dispatch

const event = v.variant({
	discriminator: 'type',
	variants: {
		click: v.object({ type: v.literal('click'), x: v.number(), y: v.number() }),
		keypress: v.object({ type: v.literal('keypress'), key: v.string() }),
	},
})

variant() reads an own discriminator, uses JavaScript property-key semantics, and executes only the selected branch. Use union() when branch selection itself requires ordered validation.

Step naming

Valchecker separates API roles through naming:

  • initial steps are nouns: string(), number(), object(), looseBigint(),
  • built-in validations use isXxx(): isInteger(), isStartingWith(), isLengthAtLeast(),
  • concrete transformations use toXxx(): toTrimmed(), toNumber(), toJSONValue(),
  • generic escape hatches remain check() and transform().

This makes the valid next operations discoverable through editor autocomplete.

Type-aligned primitives

Primitive initial steps match TypeScript primitive types. number() accepts every JavaScript number, including NaN, Infinity, and -Infinity.

v.number().execute(Number.NaN) // { value: NaN }
v.number().execute(Infinity) // { value: Infinity }

Use explicit validation when the application requires a narrower runtime domain:

v.number().isFinite()
v.number().isInteger()
v.number().isFinite().isAtLeast(0).isAtMost(100)

Loose primitives

Loose primitives accept the primitive itself or its matching TypeScript template-literal representation, then normalize the output:

v.looseNumber().execute('1e3') // { value: 1000 }
v.looseBoolean().execute('false') // { value: false }
v.looseBigint().execute('-0x10') // { value: -16n }

Their input contracts correspond to:

type LooseNumberInput = number | `${number}`
type LooseBooleanInput = boolean | `${boolean}`
type LooseBigintInput = bigint | `${bigint}`

They do not perform unrestricted JavaScript coercion. For example, looseBoolean() rejects 'TRUE', 1, and arbitrary truthy values.

Template literals

templateLiteral(parts) validates a string against an assembled TypeScript template-literal type and infers that exact output type, with cross-product union expansion. Matching mirrors the TypeScript checker's placeholder split rule, not a regex.

v.templateLiteral(['ID-', v.number()]).execute('ID-42') // { value: 'ID-42' }, output `ID-${number}`
v.templateLiteral([v.number(), v.union(['px', 'em', 'rem'])]) // output `${number}px` | `${number}em` | `${number}rem`

Built-in validations

v.string().isEmpty()
v.string().isNotEmpty()
v.string().isStartingWith('prefix')
v.string().isEndingWith('.json')
v.string().isLengthAtLeast(3).isLengthAtMost(20)

v.array(v.string()).isLengthAtLeast(1)

v.number().isFinite()
v.number().isNaN()
v.number().isInteger()
v.number().isAtLeast(0).isAtMost(100)

isAtLeast() and isAtMost() apply to numbers and bigints. Length and size constraints are intentionally separate and explicit.

String formats

Dedicated isXxx() validators cover common string formats, each with its own semantic issue code (isEmail:expected_email, isUrl:expected_url, …):

v.string().isEmail()
v.string().isUrl() // http/https; override with { protocols: [...] }
v.string().isUuid()
v.string().isIp({ version: 6 })
v.string().isIsoDateTime()
v.string().isJwt()
v.string().isEmoji()

Also available: isHex(), isMac(), isHostname(), isBase64(), isBase64Url(), isCuid2(), isUlid(), isNanoid(), isIsoDate(), and isIsoTime(). Each is value-preserving and enforces only its named format.

File and Blob

file() and blob() validate File and Blob values with feature-detected globals, so they fail gracefully in environments where the constructors are absent. MIME validation uses isMimeType(), and size validation reuses the collection size steps because both expose a numeric size.

v.file()
	.isMimeType(['image/*', 'application/pdf'])
	.isSizeAtMost(5 * 1024 * 1024)

v.blob().isMimeType('application/json')

isMimeType() matches a single type or a list, supports image/*-style wildcards, and compares case-insensitively.

Primitive conversions

Native coercion steps delegate directly to JavaScript and are exposed after any output that is not already the target primitive type:

v.string().toNumber() // Number(value)
v.unknown().toBoolean() // Boolean(value)
v.object({ value: v.number() }).toBigint() // BigInt(value), with native exceptions as issues

They do not hide extra safety policy:

v.string().toNumber().execute('invalid') // { value: NaN }
v.string().toBoolean().execute('false') // { value: true }
v.bigint().toNumber().execute(9007199254740993n)
// { value: 9007199254740992 }

Native exceptions from Number() and BigInt() become structured issues. Explicit policy conversions are separate:

v.bigint().toSafeNumber()

v.string().toMappedBoolean({
	trueValues: ['Y', 'yes'],
	falseValues: ['N', 'no'],
})

v.number().toMappedBoolean({
	trueValues: [1],
	falseValues: [0],
})

Identity conversions such as number().toNumber(), boolean().toBoolean(), and bigint().toBigint() are unavailable through the state-aware API.

Other transformations

v.string().toTrimmed()
v.string().toLowercase()
v.string().toSplit(',')
v.string().toJSONValue()
v.unknown().toJSONString()
v.array(v.number()).toSorted()
v.set(v.string()).toSize()

Use transform() for arbitrary output transformations:

v.string().transform(value => ({ value }))

Custom validation

Use check() as the generic validation escape hatch:

const positive = v.number().check(value => value > 0, { message: 'Must be positive' }
)

Callbacks may be synchronous or asynchronous according to the individual step contract. Built-in callback throws and rejections become their documented operation issues.

Results

Successful execution returns the final transformed value:

type Success<T> = { value: T }

Failure returns structured issues:

type Failure<Issue> = { issues: [Issue, ...Issue[]] }

interface Issue {
	code: string
	category: 'validation' | 'operation' | 'internal'
	message: string
	path: PropertyKey[]
	payload: unknown
	context?: IssueContext[]
}

interface IssueContext {
	type: string
	[key: string]: unknown
}

Use v.isSuccess(result) and v.isFailure(result) as result type guards.

Documentation

See the complete documentation and semantic contract at the Valchecker documentation site.

License

MIT