Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

@slowclap/vkit

melnicorn894MIT0.3.0TypeScript support: included

Lightweight, strict runtime validation toolkit for TypeScript projects

typescript, validation, runtime, validator, type-guards, object-validation

readme

@slowclap/vkit: Low Footprint Validator Kit

vkit is a tiny, no-runtime-dependency library for validating unknown objects, forms, or external API responses in TypeScript.

It provides:

  • isObjectOfShape(obj, shape) - returns true or false
  • assertShape(obj, shape) - throws AggregateValidationError if invalid
  • validateFields(obj, shape) - full validation results
  • v - built-in validators (like v.isString, v.isArray, etc.)
  • v.opt - optionalized versions (accepts undefined/null)
  • arrayOf(shape) - validates arrays of items
  • optionalize(shape) - manually make any validator optional

Installation

npm install @slowclap/vkit

Usage Example

import { v, createKit, defineShape, VKit } from '@slowclap/vkit'

interface User {
  id: string
  name: string
  age?: number
  tags: string[]
}

// Define a validation shape
const userShape = defineShape<User>({
  id: v.isString,
  name: v.isString,
  age: v.opt.isNumber,
  tags: arrayOf(v.isString)
})

// Create a validation kit for the User type
const userKit: VKit<User> = createKit<User>(userShape)

// Validate safely
const unknownObj: unknown = fetchUser()

if (userKit.isObjectOfShape(unknownObj)) {
  console.log(unknownObj.name) // fully typed User
}

// Or throw rich validation errors
try {
  userKit.assertShape(unknownObj)
  console.log('valid!')
} catch (e) {
  if (e instanceof AggregateValidationError) {
    console.error(e.errors)
  }
}

Built-in Validators

Name Description
v.isString Value must be a string
v.isNumber Value must be a number
v.isInteger Value must be an integer
v.isBoolean Value must be a boolean
v.isNumericString Value must be a numeric string
v.isIntegerString Value must be an integer string
v.isBooleanString Value must be a boolean string
v.isArray Value must be an array
v.isEnum(EnumType) Value must be a valid value for EnumType
v.literally(string | number | boolean) Value must match exactly the provided value
v.dt.isISODateString Value must be a valid ISO date string

Optional Validators

All built-in validators have corresponding optional versions in the v.opt chain.

Example:

v.opt.isString(undefined) // valid
v.opt.isNumber(null) // valid
v.opt.isBoolean(undefined) // valid

Advanced: Custom Validators

You can define your own custom validators:

import { Validator } from '@slowclap/vkit'

const isUUID: Validator = (v, field) => {
  const valid = typeof v === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(v)
  return [{ field: field || '', valid: valid, value: v, message: valid ? undefined : 'Invalid UUID' }]
}

// Usage
const userShape = defineShape<User>({
  id: isUUID,
  // ... other fields
})

⚠️ Concerns and Known Limitations

Record-like Objects

There are limitations on the ability to tightly constrain record types (e.g. { [id: string]: valueType }) while allowing deep object validation. Because of this, validation occurs loosely in that if keys are not specified in the object validation shape, they will pass as if valid. For the highest degree of safety, only trust fields that are specified in the validator shape itself.

Example:

interface Library {
  [name: string]: string
}
interface MyLibrary extendLibrary {
  myName: string
}

const vkit: createKit<MyLibrary>({
  myName: v.isString,
})

// E.g. returns { myName: 'Bob', favoriteBook: 'The Giving Tree' }
const obj = getObjFromSource()
if (vkit.isObjectOfShape(obj)) {
  // ...we can trust obj.myName, but not obj.favoriteBook
}

If we have a record type that we do want to validate, we can create a validator shape for the value type, and iterate/validate manually using Object.values(obj).

Date Validation

The v.dt.isISODateString validator only validates that a string is in ISO date format (e.g., "2024-03-20T15:30:00Z"). It does not convert the string to a JavaScript Date object. If your TypeScript interface expects a Date type, you'll need to manually convert the validated string to a Date object after validation. This is typically a concern when a strongly typed Date is serialized to JSON and back. The serialization from Date to string happens automatically, but JSON won't convert it on deserialization.

Example:

interface Event {
  startTime: Date  // Note: Type is Date, not string
}

const eventShape = defineShape<Event>({
  startTime: v.dt.isISODateString  // Validates string format
})

// After validation, you need to convert:
if (eventKit.isObjectOfShape(data)) {
  const event: Event = {
    ...data,
    startTime: new Date(data.startTime)  // Convert string to Date
  }
}

When working with TypeScript interfaces that use index signatures (e.g., [key: string]: string), these cannot be directly used in object validation paths. Instead, you must use either v.isRecord or v.recordOf for validation. If you need to work with a type that has both specific properties and an index signature, you can use the RemoveIndexSignature<T> utility type to strip the index signature.

Example:

interface RecordLike {
  [key: string]: string
}

interface SpecificRecordLike extends RecordLike {
  name: string
}

interface MyObj {
  hi: string
  person: RemoveIndexSignature<SpecificRecordLike>
}

const shape = createKit<MyObj>({
  hi: v.isString,
  person: {
    name: v.isString
  }
})

Warnings

There are some warnings logged when in development mode to give the developer guidance on usage in certain circumstances. These will only be logged when NODE_ENV=development. If the developer wants to supress them, they can set VKIT_SHOW_DEVELOPMENT_WARNINGS=false.

Error Handling

vkit provides detailed validation errors via AggregateValidationError.

Each error includes:

  • field - the name of the field
  • message - human-readable message describing what went wrong
  • value - the actual value that caused the error

Example error handling:

try {
  assertShape<User>(data, userShape)
} catch (e) {
  if (e instanceof AggregateValidationError) {
    e.errors.forEach(error => {
      console.error(`Field "${error.field}": ${error.message}`)
      console.error(`Invalid value: ${error.value}`)
    })
  }
}

Philosophy

  • Tiny: No runtime dependencies. Pure TypeScript.
  • Type-Safe: Full TypeScript support with proper type inference
  • Explicit: Validation shapes match your TypeScript interfaces
  • Flexible: Easy to extend with custom validators
  • Detailed Errors: Rich error reporting for debugging
  • Zero Dependencies: No external dependencies required

License

MIT License - see LICENSE file for details