0.3.0 • Published 1 month ago

@slowclap/vkit v0.3.0

Weekly downloads
-
License
MIT
Repository
-
Last release
1 month ago

@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

NameDescription
v.isStringValue must be a string
v.isNumberValue must be a number
v.isIntegerValue must be an integer
v.isBooleanValue must be a boolean
v.isNumericStringValue must be a numeric string
v.isIntegerStringValue must be an integer string
v.isBooleanStringValue must be a boolean string
v.isArrayValue 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.isISODateStringValue 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

0.3.0

1 month ago

0.2.2

1 month ago

0.2.1

2 months ago

0.2.0

2 months ago

0.1.9

2 months ago

0.1.8

2 months ago

0.1.7

2 months ago

0.1.6

2 months ago

0.1.5

2 months ago

0.1.4

2 months ago

0.1.3

2 months ago

0.1.2

2 months ago

0.1.1

2 months ago

0.1.0

2 months ago