Skip to content

Repository files navigation

@fishka/assertions

Part of fishka.bio — free browser-based bioinformatics tools.

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('user@example.com'); // ✓
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: 'alice@example.com' }, 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(
  { 'user@example.com': '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://github.com/fishkabio/assertions

About

Assertions and Validations for TypeScript

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages