@fishka/ servitions (英语)
TypeScript 的安全运行时间断言和验证,并附有编译时间验证.
特征
- 类型安全 - 完整的脚本支持
asserts类型警卫 - 编译时间检查 - 对象评估确保所有字段都覆盖
- 两种模式 - 请求(推出错误)或验证(返回错误字符串)
- 零依赖关系 - 轻而自足
- 可编曲 - 从简单的构件中建造复杂的验证器
- 丰富的制约因素 - 矩阵长度、独特性、字符串模式、跨字段验证
- 可动摇的树 - 只导入你需要的东西
安装
npm install @fishka/assertions
或者说
yarn add @fishka/assertions
快速启动
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
核心概念
请求与验证
请求 扔出错误并使用类型脚本 asserts 类型守护 :
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());
}
验证 不丢弃返回错误消息 :
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
}
使用 主张 用于内部一致性检查。 使用 审定 用于用户输入或需要优雅处理错误.
常用案例
1. 典型类型
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. 格式请求
import { assertUuid, assertEmail, assertHexString } from '@fishka/assertions';
assertUuid('550e8400-e29b-41d4-a716-446655440000'); // ✓
assertEmail('[email protected]'); // ✓
assertHexString('a1b2c3'); // ✓
3. 对象验证
基本对象
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);
可选字段
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
};
嵌入对象
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
};
跨外地验证
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');
},
};
严格模式( 无额外字段)
assertObject(
{ name: 'Alice', age: 30, unknownField: 'oops' },
userAssertion,
undefined,
{ failOnUnknownFields: true }, // ✗ throws: "property can't be checked: unknownField"
);
4. 阵列验证
基本阵列
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);
矩阵限制
// 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"
);
对象中的矩阵
import { undefinedOr } from '@fishka/assertions';
interface User {
name: string;
tags?: string[];
}
const userAssertion: ObjectAssertion<User> = {
name: assertString,
tags: undefinedOr(arrayAssertion(assertString)),
};
5. 记录(词典)验证
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. 自定义请求
已检查的自定义请求
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"
自定义请求
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
字符串限制
import { stringAssertion } from '@fishka/assertions';
const passwordAssertion = stringAssertion({
minLength: 8,
maxLength: 64,
});
passwordAssertion('short'); // ✗ throws: "length is too small: 5 < 8"
passwordAssertion('validpassword'); // ✓
7. 用户输入验证(不投掷)
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 验证
备选案文1:API 请求验证(Express.js Server)
验证服务器侧收到的 HTTP 请求 :
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 });
});
备选案文2:API 响应验证(客户端)
验证客户端的API响应,以确保类型安全 :
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`);
奖金:快递的中间软件模式
创建可重复使用的验证中间软件 :
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 参考
原始请求
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>
格式请求
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
结构请求
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>
工厂功能
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>
校验函数( 非抛出)
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
类型预测( 检查)
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>
核心职能
assertTruthy(value: unknown, error?: string | (() => string)): asserts value
fail(error?: string | (() => string)): never // throws assertion error
高级特性
上下文传播出错
错误自动包含到失败字段的全部路径 :
const data = {
user: {
profile: {
age: 'twenty', // wrong type
},
},
};
assertObject(data, userAssertion, 'apiResponse');
// throws: "apiResponse.user.profile.age: Not a number <string:twenty>"
类型脚本编译-时间安全
ObjectAsertion 正在编译-时间验证 - TypeScript 确保所有字段覆盖:
interface User {
name: string;
age: number;
email: string;
}
const userAssertion: ObjectAssertion<User> = {
name: assertString,
age: assertNumber,
// ✗ TypeScript error: Property 'email' is missing
};
自定义出错厂
覆盖所有断言的默认错误厂:
import { setDefaultAssertionErrorFactory } from '@fishka/assertions';
setDefaultAssertionErrorFactory((message: string) => {
return new CustomError(message);
});
常见模式
正在验证配置文件
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
表格验证
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 };
}
运行时间类型缩小
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));
}
}
为什么?
- 类型安全: 存储类型Script的编译时间和运行时间安全类型系统
- 开发者经验: 以完整上下文路径清除错误消息
- 无依赖: 没有外部依赖, 最小捆绑大小
- 弹性: 在投掷断言或返回错误消息之间选择
- 可编曲: 从简单的构件构建复杂的验证器
- 作战测试: 附有111+测试的综合测试套件
许可证
麻省理工学院
捐款
欢迎提出问题和拉动请求 https://gitea.com/fishka/assertions (英语)