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

surely-ts

v1.0.0

Published

Surely — a modern, lightweight TypeScript validation library for building expressive, type-safe schemas with zero runtime dependencies.

Readme

Surely

surely-ts v1.0.0 — A modern, lightweight TypeScript validation library for building expressive, type-safe schemas with zero runtime dependencies.

npm license zero deps

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

| 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 → refinepost |


📦 Installation

npm install surely-ts
import { 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 value

This 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 (truefalse, falsetrue) |

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 112) | | .dayOfMonth(d: DayOfMonthValue) | Must be the specified day of month (131) | | .dayOfWeek(d: DayOfWeekValue) | Must be the specified weekday ("Monday""Sunday" or 17) | | .hour(h: HourValue) | Must be the specified hour (023) | | .minute(m: MinuteValue) | Must be the specified minute (059) | | .second(s: SecondValue) | Must be the specified second (059) | | .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 numbers

Validates 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 structure

Real-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)

GitHub Repository