@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)
- returnstrue
orfalse
assertShape(obj, shape)
- throwsAggregateValidationError
if invalidvalidateFields(obj, shape)
- full validation resultsv
- built-in validators (likev.isString
,v.isArray
, etc.)v.opt
- optionalized versions (accepts undefined/null)arrayOf(shape)
- validates arrays of itemsoptionalize(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 fieldmessage
- human-readable message describing what went wrongvalue
- 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