npm package: @slowclap/vkit
Description: Lightweight, strict runtime validation toolkit for TypeScript projects
Author: Your Name
Latest version: 0.3.1

# `@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

```bash
npm install @slowclap/vkit
```

## Usage Example

```ts
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 |
| `v.ignore` | Accepts any value (no validation) |

## Optional Validators

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

Example:

```ts
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:

```ts
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
})
```

### Ignoring Object Properties

You can use `v.ignore` to accept any value for specific object properties without validation:

```ts
interface Config {
  apiKey: string
  settings: any  // We want to accept any settings object
}

const configShape = defineShape<Config>({
  apiKey: v.isString,
  settings: v.ignore  // Accepts any value, no validation
})

// This will pass validation
const config = {
  apiKey: 'secret-key',
  settings: { theme: 'dark', notifications: true, unknownField: 'anything' }
}
```

## ⚠️ 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:
```ts
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:
```ts
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:
```ts
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:

```ts
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