npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fishka/assertions

v1.0.1

Published

Assertions and Validations for TypeScript

Readme

@fishka/assertions

Type-safe runtime assertions and validations for TypeScript with compile-time verification.

Features

  • Type-safe - Full TypeScript support with asserts type guards
  • Compile-time checked - ObjectAssertion ensures all fields are covered
  • Two patterns - Assertions (throw errors) or Validations (return error strings)
  • Zero dependencies - Lightweight and self-contained
  • Composable - Build complex validators from simple building blocks
  • Rich constraints - Array length, uniqueness, string patterns, cross-field validation
  • Tree-shakable - Import only what you need

Installation

npm install @fishka/assertions

or

yarn add @fishka/assertions

Quick Start

import { assertString, assertNumber, assertObject } from '@fishka/assertions';

// Basic type assertions
assertString('hello'); // ✓ passes
assertNumber(42); // ✓ passes
assertString(123); // ✗ throws: "Not a string <number:123>"

// Object validation with compile-time type safety
interface User {
  name: string;
  age: number;
}

const userAssertion: ObjectAssertion<User> = {
  name: assertString,
  age: assertNumber,
};

const data: unknown = { name: 'Alice', age: 30 };
assertObject(data, userAssertion);
// TypeScript now knows `data` is User type
console.log(data.name); // ✓ type-safe

Core Concepts

Assertions vs Validations

Assertions throw errors and use TypeScript's asserts type guards:

import { assertString, assertObject } from '@fishka/assertions';

function processUser(data: unknown) {
  assertObject(data, userAssertion); // throws if invalid
  // TypeScript now knows data is User
  console.log(data.name.toUpperCase());
}

Validations return error messages without throwing:

import { validateObject } from '@fishka/assertions';

function validateUserInput(input: unknown) {
  const error = validateObject(input, userAssertion);
  if (error) {
    return { success: false, error };
  }
  return { success: true, data: input }; // TypeScript knows input is User
}

Use assertions for internal consistency checks. Use validations for user input or when you need to handle errors gracefully.

Common Use Cases

1. Primitive Type Assertions

import { assertString, assertNumber, assertBoolean, assertDate, assertNonNullable } from '@fishka/assertions';

assertString('hello'); // ✓
assertNumber(42); // ✓
assertBoolean(true); // ✓
assertDate(new Date()); // ✓
assertNonNullable('value'); // ✓
assertNonNullable(null); // ✗ throws: "Value is null"

2. Format Assertions

import { assertUuid, assertEmail, assertHexString } from '@fishka/assertions';

assertUuid('550e8400-e29b-41d4-a716-446655440000'); // ✓
assertEmail('[email protected]'); // ✓
assertHexString('a1b2c3'); // ✓

3. Object Validation

Basic Object

import { assertObject, type ObjectAssertion } from '@fishka/assertions';

interface User {
  name: string;
  age: number;
  email: string;
}

const userAssertion: ObjectAssertion<User> = {
  name: assertString,
  age: assertNumber,
  email: assertEmail,
};

assertObject({ name: 'Alice', age: 30, email: '[email protected]' }, userAssertion);

Optional Fields

import { undefinedOr, nullOr } from '@fishka/assertions';

interface Profile {
  name: string;
  nickname?: string; // optional
  bio: string | null; // nullable
}

const profileAssertion: ObjectAssertion<Profile> = {
  name: assertString,
  nickname: undefinedOr(assertString), // allows undefined
  bio: nullOr(assertString), // allows null
};

Nested Objects

interface Address {
  street: string;
  city: string;
}

interface User {
  name: string;
  address: Address;
}

const addressAssertion: ObjectAssertion<Address> = {
  street: assertString,
  city: assertString,
};

const userAssertion: ObjectAssertion<User> = {
  name: assertString,
  address: addressAssertion, // nested object
};

Cross-Field Validation

const userAssertion: ObjectAssertion<User> = {
  name: assertString,
  age: assertNumber,
  email: assertEmail,
  $o: user => {
    // Custom validation after all fields pass
    assertTruthy(user.age >= 18, 'User must be 18 or older');
  },
};

Strict Mode (No Extra Fields)

assertObject(
  { name: 'Alice', age: 30, unknownField: 'oops' },
  userAssertion,
  undefined,
  { failOnUnknownFields: true }, // ✗ throws: "property can't be checked: unknownField"
);

4. Array Validation

Basic Arrays

import { assertArray, arrayAssertion } from '@fishka/assertions';

assertArray(['a', 'b', 'c'], assertString); // ✓
assertArray([1, 2, 3], assertNumber); // ✓

// Or create reusable array assertion
const stringArrayAssertion = arrayAssertion(assertString);
assertArray(['x', 'y'], stringArrayAssertion);

Array Constraints

// Length constraints
assertArray([1, 2, 3], assertNumber, { minLength: 2, maxLength: 5 }); // ✓
assertArray([1], assertNumber, { minLength: 2 }); // ✗ throws

// Uniqueness check
assertArray(
  ['a', 'b', 'c'],
  assertString,
  { uniqueByIdentity: v => v }, // ✓
);

assertArray(
  ['a', 'a', 'b'],
  assertString,
  { uniqueByIdentity: v => v }, // ✗ throws: "array contains non-unique elements"
);

Arrays in Objects

import { undefinedOr } from '@fishka/assertions';

interface User {
  name: string;
  tags?: string[];
}

const userAssertion: ObjectAssertion<User> = {
  name: assertString,
  tags: undefinedOr(arrayAssertion(assertString)),
};

5. Record (Dictionary) Validation

import { assertRecord, recordAssertion } from '@fishka/assertions';

// Record<string, number>
assertRecord({ a: 1, b: 2, c: 3 }, assertNumber); // ✓

// With key validation
assertRecord(
  { '[email protected]': 'Alice' },
  assertString,
  { keyAssertion: assertEmail }, // validates keys are emails
);

// Ensure key matches field in value
assertRecord(
  {
    user123: { id: 'user123', name: 'Alice' },
    user456: { id: 'user456', name: 'Bob' },
  },
  objectAssertion({ id: assertString, name: assertString }),
  { keyField: 'id' }, // ensures record key === value.id
);

6. Custom Assertions

Type-checked Custom Assertions

import { $a } from '@fishka/assertions';

// Create custom assertion with type checking
const assertPositive = $a<number>(value => value > 0, 'Must be positive');

assertPositive(42); // ✓
assertPositive(-5); // ✗ throws: "Check is failed: Must be positive"

Untyped Custom Assertions

import { $u } from '@fishka/assertions';

const assertValidDate = $u(value => value instanceof Date && !isNaN(value.getTime()), 'Must be a valid Date');

assertValidDate(new Date()); // ✓
assertValidDate(new Date('invalid')); // ✗ throws

String Constraints

import { stringAssertion } from '@fishka/assertions';

const passwordAssertion = stringAssertion({
  minLength: 8,
  maxLength: 64,
});

passwordAssertion('short'); // ✗ throws: "length is too small: 5 < 8"
passwordAssertion('validpassword'); // ✓

7. User Input Validation (Non-throwing)

import { validateObject } from '@fishka/assertions';

function handleFormSubmit(formData: unknown) {
  const error = validateObject(formData, userAssertion);

  if (error) {
    // Show error to user
    console.error('Validation failed:', error);
    return { success: false, error };
  }

  // formData is now typed as User
  saveUser(formData);
  return { success: true };
}

8. API Validation

Option 1: API Request Validation (Express.js Server)

Validate incoming HTTP requests on the server side:

import express from 'express';
import { validateObject, assertObject, type ObjectAssertion } from '@fishka/assertions';

interface CreateUserRequest {
  name: string;
  email: string;
  age: number;
  tags?: string[];
}

const createUserRequestAssertion: ObjectAssertion<CreateUserRequest> = {
  name: assertString,
  email: assertEmail,
  age: assertNumber,
  tags: undefinedOr(arrayAssertion(assertString)),
};

const app = express();
app.use(express.json());

// POST /users - Create new user
app.post('/users', (req, res) => {
  // Validate request body
  const error = validateObject(req.body, createUserRequestAssertion);
  if (error) {
    return res.status(400).json({ success: false, error: `Invalid request: ${error}` });
  }
  // req.body is now typed as CreateUserRequest
  const user = createUser(req.body);
  res.status(201).json({ success: true, data: user });
});

// GET /users/:id - Get user by ID
app.get('/users/:id', (req, res) => {
  const userId = req.params.id;
  // Validate UUID format
  assertUuid(userId, 'userId');
  const user = getUserById(userId);
  res.json({ success: true, data: user });
});

Option 2: API Response Validation (Client-side)

Validate API responses on the client to ensure type safety:

interface ApiResponse<T> {
  success: boolean;
  data: T;
  error?: string;
}

interface User {
  id: string;
  name: string;
  email: string;
  createdAt: string;
}

interface UsersListResponse {
  users: User[];
  meta: {
    total: number;
    page: number;
    pageSize: number;
  };
}

// Define response assertion
const userAssertion: ObjectAssertion<User> = {
  id: assertUuid,
  name: assertString,
  email: assertEmail,
  createdAt: assertString,
};

const usersListResponseAssertion: ObjectAssertion<UsersListResponse> = {
  users: arrayAssertion(userAssertion),
  meta: {
    total: assertNumber,
    page: assertNumber,
    pageSize: assertNumber,
  },
};

// Client function to fetch users
async function fetchUsers(page: number = 1): Promise<UsersListResponse> {
  const response = await fetch(`/api/users?page=${page}`);
  const data: unknown = await response.json();

  // Validate response structure
  assertObject(data, usersListResponseAssertion);

  // Now data is fully typed as UsersListResponse!
  return data;
}

// Usage
const { users, meta } = await fetchUsers(1);
console.log(`Loaded ${users.length} of ${meta.total} users`);

Bonus: Middleware Pattern for Express

Create reusable validation middleware:

import { Request, Response, NextFunction } from 'express';
import { validateObject, type ObjectAssertion } from '@fishka/assertions';

// Generic validation middleware
function validateBody<T>(assertion: ObjectAssertion<T>) {
  return (req: Request, res: Response, next: NextFunction) => {
    const error = validateObject(req.body, assertion);

    if (error) {
      return res.status(400).json({
        success: false,
        error: `Validation failed: ${error}`,
      });
    }

    // Body is valid, continue to route handler
    next();
  };
}

// Use the middleware
app.post('/users', validateBody(createUserRequestAssertion), (req, res) => {
  // req.body is typed and validated
  const user = createUser(req.body);
  res.status(201).json({ success: true, data: user });
});

API Reference

Primitive Assertions

assertString(value: unknown, context?: string): asserts value is string
assertNumber(value: unknown, context?: string): asserts value is number
assertBoolean(value: unknown, context?: string): asserts value is boolean
assertDate(value: unknown, context?: string): asserts value is Date
assertNonNullable<T>(value: T, context?: string): asserts value is NonNullable<T>

Format Assertions

assertUuid(value: unknown, context?: string): asserts value is string
assertHexString(value: unknown, context?: string): asserts value is string
assertEmail(value: unknown, context?: string): asserts value is string

Structural Assertions

assertObject<T>(
  value: unknown,
  assertion: ObjectAssertion<T>,
  context?: string,
  constraints?: { failOnUnknownFields?: boolean; allowedUnknownFieldNames?: string[] }
): asserts value is T

assertArray<T>(
  value: unknown,
  elementAssertion: Assertion<T>,
  constraints?: { minLength?: number; maxLength?: number; uniqueByIdentity?: (el: T) => string },
  context?: string
): asserts value is Array<T>

assertRecord<T>(
  value: unknown,
  valueAssertion: Assertion<T>,
  constraints?: { keyAssertion?: (key: string) => void; keyField?: string },
  context?: string
): asserts value is Record<string, T>

Factory Functions

arrayAssertion<T>(elementAssertion: Assertion<T>, constraints?): ValueAssertion<Array<T>>
recordAssertion<T>(valueAssertion: Assertion<T>, constraints?): ValueAssertion<Record<string, T>>
objectAssertion<T>(objAssertion: ObjectAssertion<T>): ValueAssertion<T>

$a<T>(checkFn: (value: T) => boolean, errorMsg?: string): ValueAssertion<T>
$u(checkFn: (value: unknown) => boolean, errorMsg?: string): ValueAssertion<unknown>

undefinedOr<T>(assertion: Assertion<T>): Assertion<T | undefined>
nullOr<T>(assertion: Assertion<T>): Assertion<T | null>
valueOr<T>(expectedValue: T, orAssertion: Assertion<T>): Assertion<T>

stringAssertion(constraints: { minLength?: number; maxLength?: number }): ValueAssertion<string>

Validation Functions (Non-throwing)

validateObject<T>(value: unknown, assertion: ObjectAssertion<T>, context?, constraints?): string | undefined
validateArray<T>(value: unknown, elementAssertion: Assertion<T>, constraints?, context?): string | undefined
validateRecord<T>(value: unknown, valueAssertion: Assertion<T>, constraints?, context?): string | undefined

Type Predicates (Checks)

isString(value: unknown): value is string
isNumber(value: unknown): value is number
isBoolean(value: unknown): value is boolean
isDate(value: unknown): value is Date
isUuid(value: unknown): value is string
isHexString(value: unknown): value is string
isEmail(value: unknown, constraints?): value is string
isNonNullable<T>(value: T): value is NonNullable<T>

Core Functions

assertTruthy(value: unknown, error?: string | (() => string)): asserts value
fail(error?: string | (() => string)): never  // throws assertion error

Advanced Features

Error Context Propagation

Errors automatically include the full path to the failing field:

const data = {
  user: {
    profile: {
      age: 'twenty', // wrong type
    },
  },
};

assertObject(data, userAssertion, 'apiResponse');
// throws: "apiResponse.user.profile.age: Not a number <string:twenty>"

TypeScript Compile-Time Safety

ObjectAssertion is compile-time verified - TypeScript ensures all fields are covered:

interface User {
  name: string;
  age: number;
  email: string;
}

const userAssertion: ObjectAssertion<User> = {
  name: assertString,
  age: assertNumber,
  // ✗ TypeScript error: Property 'email' is missing
};

Custom Error Factory

Override the default error factory for all assertions:

import { setDefaultAssertionErrorFactory } from '@fishka/assertions';

setDefaultAssertionErrorFactory((message: string) => {
  return new CustomError(message);
});

Common Patterns

Validating Configuration Files

interface AppConfig {
  port: number;
  host: string;
  database: {
    url: string;
    poolSize: number;
  };
  features: string[];
}

const configAssertion: ObjectAssertion<AppConfig> = {
  port: assertNumber,
  host: assertString,
  database: {
    url: assertString,
    poolSize: assertNumber,
  },
  features: arrayAssertion(assertString),
};

const config: unknown = JSON.parse(configFileContents);
assertObject(config, configAssertion);
// config is now fully typed and validated

Form Validation

function validateLoginForm(formData: FormData): { error?: string; data?: LoginData } {
  const input = {
    email: formData.get('email'),
    password: formData.get('password'),
  };

  const error = validateObject(input, loginAssertion);
  if (error) {
    return { error };
  }

  return { data: input };
}

Runtime Type Narrowing

function processValue(value: unknown) {
  if (isString(value)) {
    // TypeScript knows value is string
    console.log(value.toUpperCase());
  } else if (isNumber(value)) {
    // TypeScript knows value is number
    console.log(value.toFixed(2));
  }
}

Why @fishka/assertions?

  • Type Safety: Leverages TypeScript's type system for compile-time and runtime safety
  • Developer Experience: Clear error messages with full context path
  • Zero Dependencies: No external dependencies, minimal bundle size
  • Flexible: Choose between throwing assertions or returning error messages
  • Composable: Build complex validators from simple building blocks
  • Battle-tested: Comprehensive test suite with 111+ tests

License

MIT

Contributing

Issues and pull requests are welcome at https://gitea.com/fishka/assertions