Stacks Documentation
⌘ K
Changelog

TypeScript Validation Package

A powerful and flexible validation library for TypeScript that provides a fluent API for validating various data types.

Installation

# Using bun
bun add @stacksjs/ts-validation

# Using npm
npm install @stacksjs/ts-validation

# Using yarn
yarn add @stacksjs/ts-validation

# Using pnpm
pnpm add @stacksjs/ts-validation

Basic Usage

import { v } from'@stacks/ts-validation' '@stacks/ts-validation'

// Basic string validation
const stringValidator = v.string().min(5).max(10)
const result = stringValidator.validate('hello''hello')
console.log(result.valid) // true

// Object validation
const userValidator = v.object().shape({
  name: v.string().min(2),
  age: v.number().min(18),
  email: v.string().email()
})

const user = {
  name:'John' 'John',
  age: 25,
  email:'john@example.com' 'john@example.com'
}

const validationResult = userValidator.validate(user)
console.log(validationResult.valid) // true

Available Validators

String Validator

const validator = v.string()
  .min(5) // minimum length
  .max(10) // maximum length
  .length(7) // exact length
  .email() // validate email format
  .alphanumeric() // only letters and numbers

Number Validator

const validator = v.number()
  .min(0) // minimum value
  .max(100) // maximum value
  .integer() // must be an integer
  .float() // can be a floating point number

Here are more examples of number validation:

// Validate a positive integer
const positiveIntValidator = v.number()
  .positive()
  .integer()

// Validate a decimal number with specific options
const decimalValidator = v.number()
  .decimal({ decimal_digits:'2' '2' })

// Validate a number divisible by 5
const divisibleValidator = v.number()
  .divisibleBy(5)

// Combine multiple rules
const scoreValidator = v.number()
  .min(0)
  .max(100)
  .decimal({ decimal_digits:'1' '1' })
  .custom(value => value % 0.5 === 0,'Score must be in 0.5 increments' 'Score must be in 0.5 increments')

// Examples:
scoreValidator.validate(85.5) // valid
scoreValidator.validate(85.7) // invalid
scoreValidator.validate(-1) // invalid

Array Validator

const validator = v.array<string>()
  .min(2) // minimum length
  .max(4) // maximum length
  .length(3) // exact length
  .each(v.string().min(2)) // validate each item
  .unique() // all items must be unique

Boolean Validator

// Basic boolean validation
const boolValidator = v.boolean()

// Must be true validation
const requiredCheckbox = v.boolean()
  .isTrue()
  .custom(
    value => typeof value ==='boolean' 'boolean','Checkbox must be checked'
    'Checkbox must be checked'
  )

// Complex boolean validation
const toggleValidator = v.boolean()
  .custom(
    value => {
      // Custom validation logic
      return typeof value ==='boolean' 'boolean' && (value === true || value === false)
    },'Toggle must be either on or off'
    'Toggle must be either on or off'
  )

// Examples:
boolValidator.validate(true) // valid
boolValidator.validate(false) // valid
boolValidator.validate('true''true') // invalid
requiredCheckbox.validate(false) // invalid

Enum Validator

// Basic enum validation
const roleValidator = v.enum(['admin''admin','user' 'user','guest' 'guest'] as const)

// Enum with custom validation
const accessLevelValidator = v.enum(['read''read','write' 'write','admin' 'admin'] as const)
  .custom(
    value => value !=='admin' 'admin' || isUserAdmin(), // hypothetical function'Only administrators can have admin access'
    'Only administrators can have admin access'
  )

// Numeric enum validation
const priorityValidator = v.enum([1, 2, 3, 5, 8] as const)
  .custom(
    value => value <= getCurrentUserMaxPriority(), // hypothetical function'Priority exceeds user\'s maximum allowed priority'
    'Priority exceeds user\'s maximum allowed priority'
  )

// Examples:
type UserRole ='admin' 'admin' |'user' 'user' |'guest' 'guest'
const userRoleValidator = v.enum<UserRole>(['admin''admin','user' 'user','guest' 'guest'] as const)

userRoleValidator.validate('admin''admin') // valid
userRoleValidator.validate('superuser''superuser') // invalid
userRoleValidator.validate('guest''guest') // valid

// Using with TypeScript discriminated unions
type UserStatus ='active' 'active' |'inactive' 'inactive' |'suspended' 'suspended'
const statusValidator = v.enum<UserStatus>(['active''active','inactive' 'inactive','suspended' 'suspended'] as const)

Date Validator

const validator = v.date()
  .min(new Date('2023-01-01''2023-01-01')) // minimum date
  .max(new Date('2023-12-31''2023-12-31')) // maximum date
  .between(new Date('2023-01-01''2023-01-01'), new Date('2023-12-31''2023-12-31'))
  .isToday() // must be today
  .isWeekend() // must be weekend
  .isWeekday() // must be weekday
  .isBefore(new Date('2024-01-01''2024-01-01'))
  .isAfter(new Date('2022-12-31''2022-12-31'))

Datetime Validator

const validator = v.datetime()
  // Validates that the value is a valid Date object
  // within MySQL DATETIME range (1000-01-01 to 9999-12-31)

// Examples:
validator.validate(new Date()) // valid
validator.validate(new Date('999-12-31''999-12-31')) // invalid (before 1000-01-01)
validator.validate(new Date('10000-01-01''10000-01-01')) // invalid (after 9999-12-31)
validator.validate('2023-01-01''2023-01-01') // invalid (must be Date object)

Timestamp Validator

const validator = v.timestamp()
  // Validates that the value is a valid timestamp
  // within MySQL TIMESTAMP range (1970-01-01 to 2038-01-19)

// Examples:
validator.validate(Date.now()) // valid
validator.validate('1672531200000''1672531200000') // valid (string timestamp)
validator.validate(-1) // invalid (before 1970-01-01)
validator.validate(2147483648) // invalid (after 2038-01-19)
validator.validate('123456789''123456789') // invalid (too short)
validator.validate('12345678901234''12345678901234') // invalid (too long)

Unix Validator

const validator = v.unix()
  // Validates that the value is a valid Unix timestamp
  // Must be a positive number or string with 10-13 digits

// Examples:
validator.validate(Math.floor(Date.now() / 1000)) // valid (10 digits)
validator.validate(Date.now()) // valid (13 digits)
validator.validate('1672531200''1672531200') // valid (string timestamp)
validator.validate(-1) // invalid (negative number)
validator.validate('123456789''123456789') // invalid (too short)
validator.validate('12345678901234''12345678901234') // invalid (too long)

Object Validator

// Basic object shape
const userValidator = v.object().shape({
  username: v.string().min(3),
  age: v.number().min(18)
})

// Nested object with strict mode
const profileValidator = v.object().shape({
  user: v.object().shape({
    firstName: v.string(),
    lastName: v.string(),
    contact: v.object().shape({
      email: v.string().email(),
      phone: v.string().optional()
    })
  }),
  settings: v.object().shape({
    newsletter: v.boolean(),
    theme: v.enum(['light''light','dark' 'dark'] as const)
  })
}).strict()

// Example usage:
const profile = {
  user: {
    firstName:'John' 'John',
    lastName:'Doe' 'Doe',
    contact: {
      email:'john@example.com' 'john@example.com',
      phone:'+1234567890' '+1234567890'
    }
  },
  settings: {
    newsletter: true,
    theme:'dark' 'dark'
  }
}

const result = profileValidator.validate(profile)
console.log(result.valid) // true

// Invalid example with extra field (strict mode)
const invalidProfile = {
  user: {
    firstName:'John' 'John',
    lastName:'Doe' 'Doe',
    contact: {
      email:'john@example.com' 'john@example.com'
    },
    extraField:'not allowed' 'not allowed' // This will fail in strict mode
  },
  settings: {
    newsletter: true,
    theme:'light' 'light'
  }
}

Custom Validator

const validator = v.custom(
  (value: string) => value.startsWith('test-''test-'),'Must start with "test-"'
  'Must start with "test-"'
)

Validation Results

All validators provide detailed validation results:

const validator = v.string().min(5).max(10)
const result = validator.validate('hi''hi')

console.log(result.valid) // false
console.log(result.errors)// [{message: 'Must be at least 5 characters long'}] // [{message: 'Must be at least 5 characters long'}]

Optional Values

Any validator can be made optional:

const validator = v.string().email().optional()
const result = validator.validate(undefined)
console.log(result.valid) // true

Complex Example

Here's a more complex example showing multiple validations:

const userValidator = v.object().shape({
  name: v.string().min(2).max(50),
  email: v.string().email(),
  age: v.number().min(18).integer(),
  website: v.string().url().optional(),
  tags: v.array<string>().each(v.string()).optional(),
  address: v.object().shape({
    street: v.string(),
    city: v.string(),
    zip: v.string()
  }).optional()
})

const user = {
  name:'John Doe' 'John Doe',
  email:'john@example.com' 'john@example.com',
  age: 25,
  website:'https://example.com' 'https://example.com',
  tags: ['developer''developer','typescript' 'typescript'],
  address: {
    street:'123 Main St' '123 Main St',
    city:'New York' 'New York',
    zip:'10001' '10001'
  }
}

const result = userValidator.validate(user)
console.log(result.valid) // true

Error Handling

When validation fails, you get detailed error messages:

const validator = v.object().shape({
  name: v.string().min(2),
  age: v.number().min(18),
  email: v.string().email()
})

const result = validator.validate({
  name:'J' 'J',
  age: 16,
  email:'invalid-email' 'invalid-email'
})

console.log(result.valid) // false
console.log(result.errors)
// [//   { message: 'name: Must be at least 2 characters long' },//   { message: 'age: Must be at least 18' },//   { message: 'email: Must be a valid email address' }
//   { message: 'name: Must be at least 2 characters long' },
//   { message: 'age: Must be at least 18' },
//   { message: 'email: Must be a valid email address' }
// ]