@fishka/assertions

中文文档
1.0.1发布于 2026/01/09MITTypeScriptESM + CJS零依赖近 30 天下载 6188

Assertions and Validations for TypeScript

原始使用文档
文档目录33 节
  1. Features
  2. Installation
  3. Quick Start
  4. Core Concepts
  5. Assertions vs Validations
  6. Common Use Cases
  7. 1. Primitive Type Assertions
  8. 2. Format Assertions
  9. 3. Object Validation
  10. 4. Array Validation
  11. 5. Record (Dictionary) Validation
  12. 6. Custom Assertions
  13. 7. User Input Validation (Non-throwing)
  14. 8. API Validation
  15. API Reference
  16. Primitive Assertions
  17. Format Assertions
  18. Structural Assertions
  19. Factory Functions
  20. Validation Functions (Non-throwing)
  21. Type Predicates (Checks)
  22. Core Functions
  23. Advanced Features
  24. Error Context Propagation
  25. TypeScript Compile-Time Safety
  26. Custom Error Factory
  27. Common Patterns
  28. Validating Configuration Files
  29. Form Validation
  30. Runtime Type Narrowing
  31. Why @fishka/assertions?
  32. License
  33. Contributing

@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

版本与发布记录

@fishka/assertions 共发布 2 个版本,首次发布于 2026/01/06;过去 12 个月发布了 2 个版本。

版本发布日期距今
1.0.1latest2026/01/0910 个月前
1.0.02026/01/0610 个月前

关于 @fishka/assertions 的常见问题

@fishka/assertions 是什么?

Assertions and Validations for TypeScript

如何安装 @fishka/assertions?

在项目目录运行 npm install @fishka/assertions,或使用 pnpm add / yarn add / bun add。

@fishka/assertions 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@fishka/assertions 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@fishka/assertions 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=10。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@fishka/assertions 可以商用吗?

@fishka/assertions 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@fishka/assertions 的下载量有多少?

根据 npm 官方下载统计,@fishka/assertions 在 2026/10/15 – 2026/11/13 这 30 天内被下载 6,188 次,日均约 206 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@fishka/assertions 还在维护吗?

最新版本 1.0.1 发布于 2026/01/09(10 个月前);至今共发布 2 个版本,其中过去 12 个月发布了 2 个;首次发布于 2026/01/06。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。