surely-ts
v1.0.0
Published
Surely — a modern, lightweight TypeScript validation library for building expressive, type-safe schemas with zero runtime dependencies.
Maintainers
Readme
Surely
surely-ts v1.0.0 — A modern, lightweight TypeScript validation library for building expressive, type-safe schemas with zero runtime dependencies.
Surely is a lightweight, TypeScript-first validation library. Define schemas, validate inputs, optionally coerce and transform them, and always get a predictable result — never a thrown validation error.
- Validation should be explicit.
- Transformation should be intentional.
- Results should be structured and composable.
Table of Contents
- What Makes Surely Different
- Installation
- Quick Start
- Core Concepts
- BaseValidator API
- Schema Reference
- Advanced Patterns
- License
🌱 What Makes Surely Different
| Feature | Surely |
| ------------------------------ | ------------------------------------------------------------------------------------------ |
| Zero thrown validation | parse() returns { success: true, data } or { success: false, issues } — never throws |
| TypeScript-first inference | Every schema infers its validated type automatically |
| Composable chains | Chain transforms, apply constraints, build nested schemas |
| Lightweight | Zero runtime dependencies |
| Predictable pipeline | pre → internal validation → refine → post |
📦 Installation
npm install surely-tsimport { surely } from "surely-ts";You can also import validators directly:
import { StringValidator, NumberValidator, ObjectValidator } from "surely-ts";Works in both Node.js and browser environments. No external dependencies — only TypeScript itself.
✨ Quick Start
import { surely } from "surely-ts";
// --- Primitive validation ---
surely.string().parse("hello"); // ✅ { success: true, data: "hello" }
surely.number().parse(42); // ✅ { success: true, data: 42 }
surely.boolean().parse("true"); // ❌ { success: false, issues: [...] }
// --- Coercion (opt-in) ---
surely.number().coerce().parse("42"); // ✅ { success: true, data: 42 }
// --- Optional & Default ---
surely.number().optional().parse(undefined); // ✅ { success: true, data: undefined }
surely.string().default("guest").parse(undefined); // ✅ { success: true, data: "guest" }
// --- Chained transforms + checks ---
surely.string().trim().toUpperCase().minLength(3).parse(" hello ");
// ✅ { success: true, data: "HELLO" }
// --- Object validation ---
const userSchema = surely.object({
name: surely.string().minLength(3),
age: surely.number().gte(18),
email: surely.string().format("email").optional(),
});
const result = userSchema.parse({ name: "Alice", age: 30 });
if (result.success) {
console.log(result.data); // { name: "Alice", age: 30 }
} else {
console.error(result.issues);
}
// --- Custom refinement ---
const adultAge = surely.number().refine((n) => {
if (n >= 18) return { success: true, data: n };
return {
success: false,
issues: [{ path: "", code: "too_young", message: "Must be 18+" }],
};
});
adultAge.parse(22); // ✅ { success: true, data: 22 }
adultAge.parse(15); // ❌ { success: false, issues: [...] }🧩 Core Concepts
Result Types
Every parse() call returns a SurelyResult<T> — never throws.
type SurelyResult<T> =
| { success: true; data: T }
| { success: false; issues: SurelyIssue[] };
type SurelyIssue = {
message: string;
input?: any;
code: string;
path: string;
subIssues?: SurelyIssue[];
};This design makes Surely ideal for functional pipelines, API input parsing, and any system that demands reliability without runtime chaos.
Validation Pipeline
Every .parse() call follows a strict 5-step pipeline:
1. Default & Optional Check
├─ Input is undefined and default is set → return default value
├─ Input is undefined and optional() → return undefined
├─ Input is null and nullable() → return null
└─ Input is undefined with no default → fail
2. pre(fn)
└─ Transform the raw input before validation
3. Internal Validation & Transform
└─ Core schema logic: type checking, normalization,
coercion, built-in transforms, constraint checks
4. refine(fn)
└─ Custom validation returning SurelyResult<T>
5. post(fn)
└─ Post-processing on the validated valueThis predictable sequence means you always know why a value failed and how it was transformed.
Parse Options
type ParseOptions = {
path: string; // dot-notation path for error reporting (default: "")
abortEarly: boolean; // stop on first error (default: true)
};const result = surely.string().minLength(3).parse("hi", {
path: "user.name",
abortEarly: false,
});
// Issues will have path: "user.name"🔧 BaseValidator API
Every schema extends BaseValidator<T>. These methods are available on all validators.
Shared Methods
| Method | Returns | Description |
| ------------------------ | --------------------------------- | --------------------------------------------------------------------------- |
| .coerce() | this | Enables automatic type coercion (off by default) |
| .default(value) | this | Returns value when input is undefined |
| .pre(fn) | this | Transforms input before validation |
| .refine(fn) | this | Custom validation after internal checks (must return SurelyResult<T>) |
| .post(fn) | this | Transforms output after successful validation |
| .clone() | this | Deep-copies the validator with all configuration |
| .optional() | OptionalValidator<T> | Wraps validator; accepts undefined → returns undefined |
| .nullable() | NullableValidator<T> | Wraps validator; accepts null → returns null |
| .optionalAndNullable() | OptionalAndNullableValidator<T> | Wraps validator; accepts both undefined and null |
All chainable methods (except .optional(), .nullable(), .optionalAndNullable()) return this, enabling fluent chains:
surely.string().coerce().trim().minLength(3).default("guest");Getters
| Getter | Type | Description |
| --------------- | ---------------- | ----------------------------------------------------------------------- |
| .layerType | LayerType | Current layer: "base", "optional", "nullable", or "optionalAndNullable" |
| .defaultValue | T \| undefined | The default value if set, otherwise undefined |
Optional / Nullable / OptionalAndNullable
import { surely } from "surely-ts";
// .optional() — accepts undefined, rejects null
surely.string().optional().parse(undefined);
// ✅ { success: true, data: undefined }
surely.string().optional().parse(null);
// ❌ { success: false, data: null }
// .nullable() — accepts null, rejects undefined
surely.string().nullable().parse(null);
// ✅ { success: true, data: null }
surely.string().nullable().parse(undefined);
// ❌ { success: false, issues: [...] }
// .optionalAndNullable() — accepts both
surely.string().optionalAndNullable().parse(null);
// ✅ { success: true, data: null }
surely.string().optionalAndNullable().parse(undefined);
// ✅ { success: true, data: undefined }
// .default() — fallback for undefined input
surely.string().default("Guest").parse(undefined);
// ✅ { success: true, data: "Guest" }Note:
.optional(),.nullable(), and.optionalAndNullable()cannot be stacked. Calling one on an already-wrapped validator throws an error.
import { surely } from "surely-ts";
// .optional() — is okay on a normal validator
const optionalString = surely.string().optional();
optionalString.parse(undefined);
// ✅ { success: true, data: undefined }
// .optional() - will throw error.
optionalString.optional();
// Error("Cannot call optional() on an already optional validator.");
// .nullable() - will still throw error
optionalString.nullable();
// Error("Cannot call nullable() on an already optional validator.");Tip: To make a schema both optional and nullable, use
.optionalAndNullable()instead of stacking.optional().nullable().
Lifecycle Hooks: pre, refine, post
import { surely } from "surely-ts";
const usernameSchema = surely
.string()
.pre((v) => v?.trim()) // 1. trim raw input
.minLength(3) // 2. internal check
.refine(
(
v, // 3. custom validation
) =>
v !== "admin"
? { success: true, data: v }
: {
success: false,
issues: [{ path: "", code: "reserved", message: "Reserved name" }],
},
)
.post((v) => v.toLowerCase()) // 4. post-process
.default("guest"); // fallback for undefined
usernameSchema.parse(undefined); // ✅ { success: true, data: "guest" }
usernameSchema.parse(" ALICE "); // ✅ { success: true, data: "alice" }
usernameSchema.parse(" admin "); // ❌ reserved name
usernameSchema.parse("ab"); // ❌ too short (after trim)
const optionalUsername = usernameSchema.optional();
optionalUsername.parse(undefined); // ✅ { success: true, data: undefined }
optionalUsername.parse(" Bob "); // ✅ { success: true, data: "bob" }Bulk Parsing: parseAnArray & parseARecord
Validate arrays or record objects using a single validator:
import { surely } from "surely-ts";
// --- parseAnArray ---
const numSchema = surely.number();
numSchema.parseAnArray([1, 2, 3]);
// ✅ { success: true, data: [1, 2, 3] }
numSchema.parseAnArray([1, "two", 3]);
// ❌ { success: false, issues: [...] } — fails for index 1
// --- parseARecord ---
const strSchema = surely.string();
strSchema.parseARecord({ name: "Alice", title: "Engineer" });
// ✅ { success: true, data: { name: "Alice", title: "Engineer" } }
strSchema.parseARecord({ name: "Alice", age: 30 });
// ❌ { success: false, issues: [...] } — fails for key "age"Both accept BulkParseOptions:
type BulkParseOptions = {
path: string; // base path for errors
abortEarly: boolean; // stop on first failing item
perItemAbortEarly: boolean; // stop on first error within each item
};Type Inference: OutputOf
Extract the validated TypeScript type from any validator:
import { surely, type OutputOf } from "surely-ts";
const nameSchema = surely.string();
type Name = OutputOf<typeof nameSchema>;
// string
const optionalName = nameSchema.optional();
type OptionalName = OutputOf<typeof optionalName>;
// string | undefined
const userSchema = surely.object({
name: surely.string(),
age: surely.number(),
email: surely.string().optional(),
});
type User = OutputOf<typeof userSchema>;
// { name: string; age: number; email: string | undefined }
const partialUser = userSchema.partial();
type PartialUser = OutputOf<typeof partialUser>;
// { name?: string | undefined; age?: number | undefined; email?: string | undefined }📚 Schema Reference
🟢 BooleanValidator
// Creation
surely.boolean(); // or surely.bool()
new BooleanValidator();Coercion (.coerce()): Converts "true" / "false" (case-insensitive) to booleans, and 1 / 0 to true / false. All other values fail.
Transforms
| Method | Description |
| ----------- | -------------------------------------------------------- |
| .toggle() | Inverts the boolean (true → false, false → true) |
Checks
| Method | Description |
| ---------- | --------------- |
| .true() | Must be true |
| .false() | Must be false |
Examples
// Strict mode (default)
surely.boolean().parse(true); // ✅ true
surely.boolean().parse("true"); // ❌ not a boolean
// Coercion
surely.boolean().coerce().parse("true"); // ✅ true
surely.boolean().coerce().parse(0); // ✅ false
surely.boolean().coerce().parse("yes"); // ❌ invalid
// Toggle transform
surely.boolean().toggle().parse(true); // ✅ false
// Value checks
surely.boolean().true().parse(false); // ❌ must be true
surely.boolean().false().parse(true); // ❌ must be false🔢 NumberValidator
// Creation
surely.number(); // or surely.num()
new NumberValidator();Coercion (.coerce()): Parses numeric strings with parseFloat(). Non-coercible values fail.
Transforms
| Method | Description |
| ------------------------------------------------ | ------------------------------------------------------------ |
| .round() | Rounds to nearest integer |
| .ceil() | Rounds up to nearest integer |
| .floor() | Rounds down to nearest integer |
| .roundMode(mode: "round" \| "floor" \| "ceil") | Sets rounding mode |
| .clamp(min: number, max: number) | Constrains value within [min, max] (throws if min > max) |
| .clampMin(min: number) | Sets lower bound only |
| .clampMax(max: number) | Sets upper bound only |
Checks
| Method | Description |
| ------------------------------------ | -------------------------------------------------------------------------------------- |
| .lt(n: number) | Less than n (exclusive) |
| .lte(n: number) | Less than or equal to n |
| .gt(n: number) | Greater than n (exclusive) |
| .gte(n: number) | Greater than or equal to n |
| .eq(n: number) | Exactly equal to n |
| .ne(n: number) | Not equal to n |
| .oneOf(values: number[]) | Must be one of the given numbers. Can be called multiple times to expand the list. |
| .notOneOf(values: number[]) | Must not be any of the given numbers. Can be called multiple times to expand the list. |
| .positive() | Must be > 0 |
| .negative() | Must be < 0 |
| .sign(s: "positive" \| "negative") | "positive" | "negative" |
| .int() | Must be an integer |
| .float() | Must be a non-integer float |
| .kind(k: "integer" \| "float") | "integer" | "float" |
| .even() | Must be even |
| .odd() | Must be odd |
| .parity(p: "even" \| "odd") | "even" | "odd" |
| .multipleOf(f: number) | Must divide evenly by f (throws if f is 0 or non-finite) |
Examples
// Strict validation
surely.number().parse(42); // ✅ 42
surely.number().parse("42"); // ❌ not a number
// Coercion
surely.number().coerce().parse("12.5"); // ✅ 12.5
// Rounding
surely.number().round().parse(4.6); // ✅ 5
surely.number().ceil().parse(4.1); // ✅ 5
surely.number().floor().parse(4.9); // ✅ 4
// Clamping
surely.number().clamp(0, 100).parse(150); // ✅ 100 (clamped to max)
surely.number().clamp(0, 100).parse(-5); // ✅ 0 (clamped to min)
// Bounds
surely.number().gt(0).lt(100).parse(50); // ✅ 50
surely.number().gte(18).parse(17); // ❌ less than 18
// Value matching
surely.number().eq(42).parse(42); // ✅ 42
surely.number().oneOf([1, 2, 3]).parse(4); // ❌
// Type & parity
surely.number().int().parse(5.5); // ❌ not integer
surely.number().even().parse(7); // ❌ not even
surely.number().positive().parse(-3); // ❌ not positive
surely.number().multipleOf(5).parse(22); // ❌ not divisible by 5📜 StringValidator
// Creation
surely.string(); // or surely.str()
new StringValidator();Coercion (.coerce()): Converts numbers and bigints to strings, booleans to "true" / "false", valid Dates to ISO strings.
Transforms
| Method | Description |
| --------------------------------------------------------- | ------------------------------------------------------ |
| .trim() | Removes leading and trailing whitespace |
| .toUpperCase() | Converts to uppercase |
| .toLowerCase() | Converts to lowercase |
| .casingMode(mode: "upper" \| "lower") | Sets casing: "upper" | "lower" |
| .prefix(str) | Prepends str to the value |
| .suffix(str) | Appends str to the value |
| .replace(search: string \| RegExp, replacement: string) | Replaces matches (chainable for multiple replacements) |
Checks
| Method | Description |
| ------------------------------------- | ---------------------------------------- |
| .minLength(n: number) | Minimum number of characters |
| .maxLength(n: number) | Maximum number of characters |
| .length(n: number) | Exact length |
| .enum(values: string[]) | Must be one of the given strings |
| .regex(pattern: RegExp) | Must match the given RegExp |
| .contains(substr: string) | Must include the substring |
| .startsWith(prefix: string) | Must start with the prefix |
| .endsWith(suffix: string) | Must end with the suffix |
| .format(pattern: StringPatternType) | Must match a built-in format (see below) |
Built-in Formats
export type StringPatternType =
| "email"
| "url"
| "uuid"
| "ip"
| "mac"
| "datetime"
| "numeric"
| "alphanumeric"
| "hex"
| "alphabetic"
| "creditCard"
| "phone";The .format(pattern) method accepts these predefined patterns:
| Pattern | Description |
| ---------------- | ----------------------------------------------------------- |
| "email" | Valid email address |
| "url" | Valid URL (http/https) |
| "uuid" | Valid UUID v4 |
| "ip" | Valid IPv4 address |
| "mac" | Valid MAC address |
| "datetime" | ISO 8601 datetime string |
| "numeric" | Contains only digits (0-9) |
| "alphanumeric" | Contains only letters and digits |
| "alphabetic" | Contains only letters (a-z, A-Z) |
| "hex" | Valid hexadecimal string |
| "creditCard" | Valid credit card number (Visa, Mastercard, Amex, Discover) |
| "phone" | Valid international phone number (E.164) |
Examples
// Basic validation
surely.string().parse("hello"); // ✅ "hello"
surely.string().parse(123); // ❌ not a string
// Coercion
surely.string().coerce().parse(42); // ✅ "42"
surely.string().coerce().parse(true); // ✅ "true"
// Transforms (applied in order)
surely.string().trim().parse(" hello "); // ✅ "hello"
surely.string().toUpperCase().parse("abc"); // ✅ "ABC"
surely.string().toLowerCase().parse("ABC"); // ✅ "abc"
surely.string().prefix("Mr. ").parse("Smith"); // ✅ "Mr. Smith"
surely.string().suffix("!").parse("Hello"); // ✅ "Hello!"
// Chained replacements
surely
.string()
.replace("world", "there")
.replace(/!/g, ".")
.parse("hello world!"); // ✅ "hello there."
// Length checks
surely.string().minLength(3).parse("ab"); // ❌ too short
surely.string().maxLength(5).parse("toolong"); // ❌ too long
surely.string().length(4).parse("test"); // ✅ exact match
// String enum
surely.string().enum(["cat", "dog"]).parse("fish"); // ❌ not in enum
// Pattern matching
surely
.string()
.regex(/^[A-Z]+$/)
.parse("abc"); // ❌ doesn't match
surely.string().contains("foo").parse("bar"); // ❌ missing substring
surely.string().startsWith("pre").parse("test"); // ❌ doesn't start with
surely.string().endsWith("end").parse("start"); // ❌ doesn't end with
// Built-in format validation
surely.string().format("email").parse("[email protected]"); // ✅
surely.string().format("url").parse("https://example.com"); // ✅
surely.string().format("uuid").parse("550e8400-e29b-41d4-a716-446655440000"); // ✅
surely.string().format("ip").parse("192.168.1.1"); // ✅
// Combining transforms + checks
surely
.string()
.trim()
.toLowerCase()
.minLength(3)
.maxLength(20)
.format("alphanumeric")
.parse(" HelloWorld123 "); // ✅ "helloworld123"🗓️ DateValidator
// Creation
surely.date(); // or surely.dt()
new DateValidator();Coercion (.coerce()): Parses ISO date strings, timestamp strings, and numbers via new Date(). Invalid dates are rejected.
Transforms
| Method | Description |
| --------------------------------------- | --------------------------------------------------- |
| .offset(offset: DateOffset) | Shifts date by time units (see DateOffset below) |
| .startOf(unit: DateBoundaryUnit) | Snaps to the start of the given boundary unit |
| .endOf(unit: DateBoundaryUnit) | Snaps to the end of the given boundary unit |
| .dateOnly() | Alias for .startOf("day") — strips time component |
| .boundaryMode(mode: DateBoundaryMode) | Sets boundary directly via { unit, kind } object |
DateOffset:
type DateOffset = {
years?: number;
months?: number;
days?: number;
hours?: number;
minutes?: number;
seconds?: number;
milliseconds?: number;
};
type DateBoundaryUnit = "year" | "month" | "day" | "hour" | "minute" | "second";
type DateBoundaryKind = "startOf" | "endOf";
type DateBoundaryMode = {
unit: DateBoundaryUnit;
kind: DateBoundaryKind;
};Checks — Date Comparisons
| Method | Description |
| --------------------------------------------------------- | -------------------------------------------------------------------------- |
| .before(date: Date, options?: DateBoundaryOptions) | Must be earlier than date |
| .after(date: Date, options?: DateBoundaryOptions) | Must be later than date |
| .between(start: DateBoundary, end: DateBoundary) | Alias for .after(start.date, { ...start }).before(end.date, { ...end }) |
| .equals(date: Date, boundaryMode?: DateBoundaryMode) | Alias for .between({ date, inclusive: true }, { date, inclusive: true }) |
| .notEquals(date: Date, boundaryMode?: DateBoundaryMode) | Must not equal date |
DateBoundaryOptions:
type DateBoundaryOptions = {
inclusive?: boolean; // include the boundary date (default: false)
boundary?: DateBoundaryMode; // apply boundary snapping before comparison
};
type DateBoundary = DateBoundaryOptions & {
date: Date;
};Checks — Date Parts
| Method | Description |
| ----------------------------------- | --------------------------------------------------------------------- |
| .year(n: number) | Must be in the specified year |
| .month(m: MonthInput) | Must be in the specified month ("January"–"December" or 1–12) |
| .dayOfMonth(d: DayOfMonthValue) | Must be the specified day of month (1–31) |
| .dayOfWeek(d: DayOfWeekValue) | Must be the specified weekday ("Monday"–"Sunday" or 1–7) |
| .hour(h: HourValue) | Must be the specified hour (0–23) |
| .minute(m: MinuteValue) | Must be the specified minute (0–59) |
| .second(s: SecondValue) | Must be the specified second (0–59) |
| .millisecond(ms: number) | Must be the specified millisecond |
| .parts(parts: Partial<DateParts>) | Check multiple date parts at once (accepts Partial<DateParts>) |
DateParts:
type DateParts = {
year: number;
month: MonthInput;
dayOfMonth: DayOfMonthValue;
dayOfWeek: DayOfWeekValue;
hour: HourValue;
minute: MinuteValue;
second: SecondValue;
millisecond: number;
};
export type MonthInput = MonthName | MonthValue;
export type MonthName = "January"| "February"| "March"| ... | "October"| "November"| "December";
export type MonthValue = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
export type DayOfMonthValue = 1 | 2 | 3 | ... | 27 | 28 | 29 | 30 | 31;
export type DayOfWeekInput = DayOfWeekName | DayOfWeekValue;
export type DayOfWeekName = "Monday"| "Tuesday"| "Wednesday"| "Thursday"| "Friday"| "Saturday"| "Sunday";
export type DayOfWeekValue = 1 | 2 | 3 | 4 | 5 | 6 | 7;
export type HourValue = 0 | 1 | 2 | 3 | ... | 21 | 22 | 23;
export type MinuteValue = 0 | 1 | 2 | 3 | ... | 57 | 58 | 59;
export type SecondValue = MinuteValue;
Examples
// Basic validation
surely.date().parse(new Date()); // ✅
surely.date().parse("2024-01-01"); // ❌ strict mode rejects strings
// Coercion from strings & timestamps
surely.date().coerce().parse("2024-12-25"); // ✅ Date object
surely.date().coerce().parse(1700000000000); // ✅ Date from timestamp
surely.date().coerce().parse("not-a-date"); // ❌ invalid date
// Transforms
surely
.date()
.offset({ days: 1, hours: 2 })
.parse(new Date("2024-01-01T00:00:00Z"));
// ✅ Date shifted to 2024-01-02T02:00:00Z
surely.date().dateOnly().parse(new Date("2024-06-15T14:30:00Z"));
// ✅ Date snapped to 2024-06-15T00:00:00Z
surely.date().startOf("month").parse(new Date("2024-06-15"));
// ✅ Date snapped to 2024-06-01T00:00:00Z
// Before / After
const deadline = new Date("2025-01-01");
surely.date().before(deadline).parse(new Date("2024-06-01")); // ✅ before deadline
surely.date().after(deadline).parse(new Date("2024-06-01")); // ❌ not after deadline
// Inclusive comparison
surely.date().before(deadline, { inclusive: true }).parse(deadline); // ✅
// Between (auto-orders)
surely
.date()
.between(
{ date: new Date("2024-01-01"), inclusive: true },
{ date: new Date("2024-12-31"), inclusive: true },
)
.parse(new Date("2024-06-15")); // ✅ within range
// Equals with boundary
surely
.date()
.equals(new Date("2024-06-15T14:30:00Z"), { unit: "day", kind: "startOf" })
.parse(new Date("2024-06-15T08:00:00Z")); // ✅ same day
// Date parts
surely.date().year(2024).parse(new Date("2024-06-15")); // ✅
surely.date().month("June").parse(new Date("2024-06-15")); // ✅
surely.date().month(6).parse(new Date("2024-06-15")); // ✅
surely.date().dayOfWeek("Monday").parse(someMonday); // ✅
surely.date().dayOfMonth(15).parse(new Date("2024-06-15")); // ✅
// Multiple parts at once
surely
.date()
.parts({
year: 2024,
month: "December",
dayOfMonth: 25,
})
.parse(new Date("2024-12-25T10:00:00Z")); // ✅ Christmas 2024🔠 EnumValidator
// Creation
surely.enum(["red", "green", "blue"] as const);Validates that the input is one of the specified literal values. Uses strict equality (===).
Requires at least one element (throws on empty array).
Examples
// String enum
const colorSchema = surely.enum(["red", "green", "blue"] as const);
colorSchema.parse("red"); // ✅ "red"
colorSchema.parse("yellow"); // ❌ not in enum
// Number enum
const levelSchema = surely.enum([1, 2, 3] as const);
levelSchema.parse(2); // ✅ 2
levelSchema.parse(4); // ❌ not in enum
// Mixed (strict equality)
const mixedSchema = surely.enum(["a", 2, "c"] as const);
mixedSchema.parse(2); // ✅
mixedSchema.parse("2"); // ❌ "2" !== 2🏷️ NativeEnumValidator
// Creation
surely.nativeEnum(MyEnum);Validates against TypeScript enum objects (both string and numeric enums). Extracts the enum values automatically.
Requires at least one value (throws on empty enum).
Examples
// String enum
enum Color {
Red = "red",
Green = "green",
Blue = "blue",
}
surely.nativeEnum(Color).parse("red"); // ✅ "red"
surely.nativeEnum(Color).parse("yellow"); // ❌ not a Color value
surely.nativeEnum(Color).parse("Red"); // ❌ "Red" is the key, not the value
// Numeric enum
enum Status {
Active, // 0
Inactive, // 1
Pending, // 2
}
surely.nativeEnum(Status).parse(0); // ✅ 0
surely.nativeEnum(Status).parse(5); // ❌ not a Status value
surely.nativeEnum(Status).parse("Active"); // ❌ strings rejected in numeric enum📦 ArrayValidator
// Creation
surely.array(surely.string()); // array of strings
surely.array(surely.number()); // array of numbersValidates that the input is an array and validates each item against the given validator.
Coercion (.coerce()): Splits strings by a separator (default ",") and trims each item.
Transforms
| Method | Description |
| ------------------------- | ------------------------------------------------------------- |
| .compact() | Removes null and undefined items from the array |
| .separator(sep: string) | Sets the split separator for string coercion (default: ",") |
Checks
| Method | Description |
| ----------------------- | ------------------------------------------------ |
| .minLength(n: number) | Minimum number of items |
| .maxLength(n: number) | Maximum number of items |
| .length(n: number) | Exact item count |
| .nonEmpty() | Alias for .minLength(1) |
| .distinct() | All items must be unique (uses Set comparison) |
Examples
// Basic array validation
surely.array(surely.number()).parse([1, 2, 3]);
// ✅ [1, 2, 3]
surely.array(surely.number()).parse([1, "two", 3]);
// ❌ fails for index 1
// Coercion (split strings)
surely.array(surely.string()).coerce().parse("a,b,c");
// ✅ ["a", "b", "c"]
surely.array(surely.number().coerce()).coerce().separator("|").parse("1|2|3");
// ✅ [1, 2, 3]
// Compact (remove nullish values)
surely.array(surely.number()).compact().parse([1, null, 2, undefined, 3]);
// ✅ [1, 2, 3]
// Length checks
surely.array(surely.string()).minLength(2).parse(["a"]);
// ❌ too few items
surely.array(surely.string()).maxLength(3).parse(["a", "b", "c", "d"]);
// ❌ too many items
surely.array(surely.string()).nonEmpty().parse([]);
// ❌ empty array
// Distinct
surely.array(surely.number()).distinct().parse([1, 2, 2, 3]);
// ❌ duplicate value
surely.array(surely.number()).distinct().parse([1, 2, 3]);
// ✅ all unique📐 TupleValidator
// Creation
surely.tuple([surely.string(), surely.number(), surely.boolean()]);Validates fixed-length arrays where each position has a specific validator.
Coercion (.coerce()): Splits strings by a separator (default ",") and trims each item.
Methods
| Method | Description |
| -------------------------------------- | ----------------------------------------------------------------------------- |
| .separator(sep: string) | Sets the split separator for string coercion |
| .rest(validator: BaseValidator<any>) | Adds a rest element validator for additional items beyond the fixed positions |
Examples
// Fixed-length tuple
const coordSchema = surely.tuple([surely.number(), surely.number()]);
coordSchema.parse([10, 20]); // ✅ [10, 20]
coordSchema.parse([10]); // ❌ wrong length
coordSchema.parse([10, 20, 30]); // ❌ wrong length
// Typed positions
const entrySchema = surely.tuple([
surely.string(), // name
surely.number(), // age
surely.boolean(), // active
]);
entrySchema.parse(["Alice", 30, true]); // ✅
entrySchema.parse(["Alice", "30", true]); // ❌ index 1 is not a number
// Rest element (variadic tuple)
const argsSchema = surely.tuple([surely.string()]).rest(surely.number());
argsSchema.parse(["cmd", 1, 2, 3]); // ✅ ["cmd", 1, 2, 3]
argsSchema.parse(["cmd"]); // ✅ ["cmd"]
argsSchema.parse([]); // ❌ missing fixed element
argsSchema.parse(["cmd", "nope"]); // ❌ rest elements must be numbers
// Coercion with custom separator
surely
.tuple([surely.string(), surely.string()])
.coerce()
.separator("|")
.parse("hello|world"); // ✅ ["hello", "world"]🏗️ ObjectValidator
// Creation
surely.object({
name: surely.string(),
age: surely.number(),
email: surely.string().optional(),
});Validates objects against a defined shape. Each key maps to a validator.
By default, unknown keys are silently stripped from the output.
Shape Transformation Methods
| Method | Returns | Description |
| ----------------------------------- | ------------------------------------ | -------------------------------------------------- |
| .partial() | ObjectValidator<PartialShape<T>> | Makes all fields optional |
| .required() | ObjectValidator<RequiredShape<T>> | Removes optional/nullable wrappers from all fields |
| .pick(keys: (keyof T)[]) | ObjectValidator<PickShape<T, K>> | Keeps only the specified fields |
| .omit(keys: (keyof T)[]) | ObjectValidator<OmitShape<T, K>> | Removes the specified fields |
| .extend(shape: U) | ObjectValidator<ExtendShape<T, U>> | Adds or overrides fields |
| .merge(other: ObjectValidator<U>) | ObjectValidator<MergeShape<T, U>> | Merges with another ObjectValidator |
Unknown Keys Handling
| Method | Description |
| ----------------------- | ------------------------------------------------- |
| .strip() | Silently removes unrecognized keys (default) |
| .unknownKeys("fail") | Fails validation if unrecognized keys are present |
| .unknownKeys("strip") | Explicitly sets strip mode |
Other
| Property | Description |
| ------------- | ---------------------------------------------------------- |
| .schemaKeys | Getter — returns the keys of the object schema as an array |
Examples
const userSchema = surely.object({
name: surely.string().minLength(1),
age: surely.number().gte(0),
email: surely.string().format("email").optional(),
});
// Basic validation
userSchema.parse({ name: "Alice", age: 30 });
// ✅ { name: "Alice", age: 30 }
userSchema.parse({ name: "", age: 30 });
// ❌ name too short
// Extra keys are stripped by default
userSchema.parse({ name: "Alice", age: 30, foo: "bar" });
// ✅ { name: "Alice", age: 30 } — "foo" is removed
// Fail on unknown keys
userSchema.unknownKeys("fail").parse({ name: "Alice", age: 30, foo: "bar" });
// ❌ unrecognized key "foo"
// Coercion on inner validators
const coercedSchema = surely.object({
name: surely.string(),
age: surely.number().coerce(),
active: surely.boolean().coerce(),
});
coercedSchema.parse({ name: "Alice", age: "30", active: "true" });
// ✅ { name: "Alice", age: 30, active: true }
// --- Shape transformations ---
// partial() — all fields become optional
const partialUser = userSchema.partial();
partialUser.parse({}); // ✅ {}
partialUser.parse({ name: "Alice" }); // ✅ { name: "Alice" }
// required() — remove optional wrappers
const strictUser = partialUser.required();
strictUser.parse({ name: "Alice", age: 30, email: "[email protected]" }); // ✅
// pick() — keep only selected fields
const nameOnly = userSchema.pick(["name"]);
nameOnly.parse({ name: "Alice" }); // ✅
nameOnly.parse({ name: "Alice", age: 30 }); // ✅ (age stripped)
// omit() — remove selected fields
const noEmail = userSchema.omit(["email"]);
noEmail.parse({ name: "Alice", age: 30 }); // ✅
// extend() — add new fields
const extendedUser = userSchema.extend({
role: surely.string().enum(["admin", "user"]),
});
extendedUser.parse({ name: "Alice", age: 30, role: "admin" }); // ✅
// merge() — combine two object validators
const addressSchema = surely.object({
street: surely.string(),
city: surely.string(),
});
const fullProfile = userSchema.merge(addressSchema);
fullProfile.parse({
name: "Alice",
age: 30,
street: "123 Main St",
city: "Anytown",
}); // ✅
// Schema introspection
userSchema.schemaKeys; // ["name", "age", "email"]🔀 UnionValidator
// Creation
surely.union([surely.string(), surely.number()]);Tries each validator in order and returns the first successful result. If all validators fail, returns all accumulated issues.
Requires at least one validator (throws on empty array).
Properties
| Property | Description |
| ------------- | ------------------------------ |
| .validators | The array of member validators |
Examples
const strOrNum = surely.union([surely.string(), surely.number()]);
strOrNum.parse("hello"); // ✅ "hello"
strOrNum.parse(42); // ✅ 42
strOrNum.parse(true); // ❌ no match — returns issues from both validators
// Complex union
const responseSchema = surely.union([
surely.object({
status: surely.enum(["ok"] as const),
data: surely.string(),
}),
surely.object({
status: surely.enum(["error"] as const),
message: surely.string(),
}),
]);
responseSchema.parse({ status: "ok", data: "hello" }); // ✅
responseSchema.parse({ status: "error", message: "failed" }); // ✅
responseSchema.parse({ status: "unknown" }); // ❌
// Union with optional
const nullableString = surely
.union([surely.string(), surely.number()])
.optional();
nullableString.parse(undefined); // ✅ undefined
nullableString.parse("hello"); // ✅ "hello"🧠 Advanced Patterns
Nested Object Validation
const addressSchema = surely.object({
street: surely.string().minLength(1),
city: surely.string().minLength(1),
zip: surely.string().regex(/^\d{5}$/),
});
const companySchema = surely.object({
name: surely.string(),
founded: surely.number().int().gte(1800),
});
const employeeSchema = surely.object({
name: surely.string().minLength(2),
age: surely.number().int().gte(18).lt(120),
email: surely.string().format("email"),
address: addressSchema,
company: companySchema.optional(),
});
const result = employeeSchema.parse({
name: "Alice",
age: 30,
email: "[email protected]",
address: {
street: "123 Main St",
city: "Springfield",
zip: "62704",
},
});
// ✅ fully validated nested structureReal-World Form Validation
const registrationSchema = surely.object({
username: surely
.string()
.trim()
.toLowerCase()
.minLength(3)
.maxLength(20)
.format("alphanumeric"),
password: surely.string().minLength(8).maxLength(128),
email: surely.string().trim().toLowerCase().format("email"),
age: surely.number().int().gte(13).lt(150),
acceptedTerms: surely.boolean().true(),
referralCode: surely.string().length(8).format("alphanumeric").optional(),
});
const input = {
username: " AliceB ",
password: "securePass123",
email: " [email protected] ",
age: 25,
acceptedTerms: true,
};
const result = registrationSchema.parse(input);
// ✅ {
// username: "aliceb",
// password: "securePass123",
// email: "[email protected]",
// age: 25,
// acceptedTerms: true,
// }Transform Pipeline Example
// Input: messy price string → Output: clean cents integer
const priceSchema = surely
.string()
.trim()
.replace("$", "")
.replace(",", "")
.refine((v) => {
const n = parseFloat(v);
if (isNaN(n)) {
return {
success: false,
issues: [
{ path: "", code: "invalid_price", message: "Not a valid price" },
],
};
}
return { success: true, data: v };
})
.post((v) => String(Math.round(parseFloat(v) * 100)));
priceSchema.parse("$1,234.56"); // ✅ "123456" (cents)Cloning & Reuse
const baseString = surely.string().trim().minLength(1).maxLength(255);
// Clone preserves all configuration
const nameField = baseString.clone();
const titleField = baseString.clone().maxLength(100);
const descriptionField = baseString.clone().maxLength(5000);📜 License
MIT © 2025 yohannes.codes (Surely)
