@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
assertstype 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/assertionsor
yarn add @fishka/assertionsQuick 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-safeCore 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')); // ✗ throwsString 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 stringStructural 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 | undefinedType 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 errorAdvanced 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 validatedForm 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
