Custom Validators
Build your own validators with createValidator, applyChain, and validation helpers.
Custom Validators
When the built-in validators don't cover your use case, you can build custom ones using createValidator() and the helper functions.
createValidator()
Creates a base validator with a custom parse function:
function createValidator<T>(
parse: (input: unknown, context: ParseContext) => ParseResult<T>,
opts?: { typeLabel?: string },
): Validator<T>
The parse function receives:
input— the raw value (alwaysunknown)context—{ key: string }(the env variable name, useful for error messages)
It must return a ParseResult<T>:
parseOk(value)— validation passedsingleError(error)— validation failed with a single errorparseFail(errors)— validation failed with multiple errors
Helper Functions
parseOk(value)
Signals successful parsing:
import { parseOk } from "@ctroenv/core"
return parseOk("parsed-value")
singleError(error)
Signals a single validation error:
import { singleError } from "@ctroenv/core"
return singleError({
key: ctx.key,
message: "Value must be at least 10 characters",
code: "invalid_value",
})
parseFail(errors)
Signals multiple validation errors:
import { parseFail } from "@ctroenv/core"
return parseFail([
{ key: ctx.key, message: "First error", code: "invalid_value" },
{ key: ctx.key, message: "Second error", code: "invalid_value" },
])
errInvalid(key, value, message)
Creates a ValidationError for an invalid value:
import { errInvalid } from "@ctroenv/core"
return singleError(errInvalid(ctx.key, input, "not a valid format"))
errType(key, received, expected)
Creates a ValidationError for a type mismatch:
import { errType } from "@ctroenv/core"
return singleError(errType(ctx.key, typeof input, "semver"))
errMissing(key)
Creates a ValidationError for a missing required value:
import { errMissing } from "@ctroenv/core"
return singleError(errMissing(ctx.key))
errWrap(error, key)
Wraps a generic Error into a ValidationError:
import { errWrap } from "@ctroenv/core"
try { riskyOperation() }
catch (e) { return singleError(errWrap(e, ctx.key)) }
applyChain()
Wraps a base validator with the standard chainable methods (.optional(), .default(), .describe(), .secret(), .validate()):
function applyChain<T>(validator: Validator<T>): Validator<T> & ChainableMethods<T>
Full Example
Building a semver() validator from scratch:
import {
createValidator, applyChain, parseOk, singleError,
errInvalid, errType,
} from "@ctroenv/core"
function semver() {
const base = createValidator<string>(
(input, ctx) => {
if (typeof input !== "string") {
return singleError(errType(ctx.key, typeof input, "semver"))
}
if (!/^\d+\.\d+\.\d+$/.test(input)) {
return singleError(errInvalid(ctx.key, input, "not a valid semver"))
}
return parseOk(input)
},
{ typeLabel: "semver" },
)
return applyChain(base)
}
// Usage:
const env = defineEnv({
APP_VERSION: semver(),
API_VERSION: semver().optional(),
})
Types
import type {
Validator, // Generic validator: { parse(input, ctx): ParseResult<T>, metadata: ValidatorMetadata }
ParseResult, // ParseResultOk<T> | ParseResultFail
ParseContext, // { key: string }
ValidatorMetadata, // { typeLabel, isSecret, isOptional, hasDefault, defaultValue, description }
} from "@ctroenv/core"How is this guide?
Last updated on Jun 25, 2026