defuss-transval
v1.3.0
Published
A fast, functional, reliable and small state (e.g. form) validation library that aims for correctness and simplicity.
Maintainers
Readme
Powerful Data Transformation and Validation
defuss-transvalprovides a flexible, chainable API for data transformation and validation in JavaScript and TypeScript applications.It supports a wide range of built-in validators and transformers, allows for custom validators to be easily added while maintaining type safety, and is designed for both synchronous and asynchronous validation scenarios.
The library follows a field-path based approach where you create validation rules for specific data paths using either string paths or type-safe PathAccessor objects, apply transformations and validations in a chainable manner, and then execute them against your data. When validation succeeds, you get access to the transformed data with type safety guarantees.
Features
- Fluent API: Chain validators together for a clean and readable validation syntax
- Type Safety: Written in TypeScript with strong type definitions and PathAccessor support for type-safe field paths
- Extensible: Easily add custom validators through the registry system
- Parameterized Validators: Support for validators that take parameters
- Custom Error Messages: Customize error messages for each validator or chain
- Translation Support: Built-in support for translating error messages
- Async Support: All validation operations return Promises for consistent async programming
- PathAccessor Integration: Use type-safe field paths with auto-completion and refactoring support
Basic Usage
import { rule, transval, Rules, access } from 'defuss-transval';
type LoginForm = {
email: string;
password: string;
rememberMe: string; // "on" | "yes" | "1" | "true"
};
const form = access<LoginForm>();
// Custom validator to check email availability
class LoginValidators extends Rules {
async isEmailAvailable() {
return (async (email: string) => {
const response = await fetch(`/api/check-email?email=${email}`);
const { available } = await response.json();
return available || "Email is already registered";
}) as unknown as Rules & this;
}
}
// Extend rule with custom validators
const loginRule = rule.extend(LoginValidators);
// Create validation with PathAccessor - clean and type-safe
const { isValid, getMessages, getData } = transval(
loginRule(form.email).isString().isEmail().isEmailAvailable(),
loginRule(form.password).isString().isLongerThan(8),
loginRule(form.rememberMe).isString().asBoolean() // Transforms "on" | "yes" | "1" | "true" to boolean true
);
// Example form data (usually read using dequery as: $(...).form())
const formData: LoginForm = {
email: "[email protected]",
password: "mypassword123",
rememberMe: "true"
};
// Validate and get transformed data in just a few lines
if (await isValid(formData)) {
const data = getData(); // Get all transformed data at once
console.log('Login successful!', data);
// cleanData.rememberMe is now boolean true, not string "true"
} else {
// render validation messages (you can use a custom formatter using JSX, defuss, React, etc.)
for (const message of getMessages()) {
console.error(`Error in field ${message.path}: ${message.message}`);
// e.g. <FieldError key={message.path} message={message.message} />
}
// getMessages() might be: [{ message: "Email is already registered", path: "email" }]
}PathAccessor Support
defuss-transval supports both string-based and object-based path access. This provides better type safety and auto-completion when working with typed data structures.
import { rule, transval, access } from 'defuss-transval';
type UserData = {
user: {
profile: {
name: string;
email: string;
settings: {
theme: 'light' | 'dark';
notifications: boolean;
};
};
posts: Array<{
title: string;
published: boolean;
}>;
};
};
const userData: UserData = {
user: {
profile: {
name: 'John Doe',
email: '[email protected]',
settings: {
theme: 'dark',
notifications: true
}
},
posts: [
{ title: 'First Post', published: true },
{ title: 'Draft Post', published: false }
]
}
};
const o = access<UserData>();
// setup rules using PathAccessors for type-safe field paths
const { isValid, getMessages } = transval(
rule(o.user.profile.name)
.asString()
.isLongerThan(2),
rule(o.user.profile.email)
.asString()
.isEmail(),
rule(o.user.profile.settings.theme)
.asString()
.isOneOf(['light', 'dark']),
rule(o.user.posts[0].title)
.asString()
.isLongerThan(5)
);
if (await isValid(userData)) {
console.log('User data is valid!');
// Access transformed values using PathAccessor
const transformedName = nameRule.getField(o.user.profile.name);
const transformedEmail = emailRule.getField(o.user.profile.email);
console.log('Transformed data:', {
name: transformedName,
email: transformedEmail
});
} else {
// Get validation messages for specific PathAccessor fields
const nameErrors = getMessages(o.user.profile.name);
const emailErrors = getMessages(o.user.profile.email);
console.log('Validation errors:', {
name: nameErrors,
email: emailErrors
});
}Mixed Usage
You can mix string paths and PathAccessors in the same validation:
// Mix string and PathAccessor approaches
const { isValid } = transval(
rule("user.profile.name").asString().isRequired(),
rule(o.user.profile.email).asString().isEmail(),
rule("user.posts.0.title").asString().isLongerThan(3)
);
if (await isValid(userData)) {
console.log('Mixed validation passed!');
}Benefits of PathAccessor
- Type Safety: Get compile-time checking for field paths
- Auto-completion: IDE support for discovering available fields
- Refactoring: Automatic updates when you rename fields in your types
- Runtime Safety: PathAccessor validates that paths exist at runtime
Custom Validators
You can create custom validators by extending the Rules class and use them with both string paths and PathAccessors:
import { rule, transval, Rules, access } from 'defuss-transval';
type FormData = {
user: {
email: string;
username: string;
preferences: {
newsletter: boolean;
};
};
};
const o = access<FormData>();
class CustomRules extends Rules {
checkEmail(apiEndpoint: string) {
return (async (value: string) => {
// Simulate an async API call
await new Promise((resolve) => setTimeout(resolve, 100));
return value.includes("@") && value.includes(".");
}) as unknown as Rules & this;
}
isValidUsername(minLength: number = 3) {
return ((value: string) => {
return typeof value === 'string' &&
value.length >= minLength &&
/^[a-zA-Z0-9_]+$/.test(value);
}) as unknown as Rules & this;
}
}
const formData: FormData = {
user: {
email: '[email protected]',
username: 'johndoe123',
preferences: {
newsletter: true
}
}
};
// Extend the rule function with custom validators
const customRules = rule.extend(CustomRules);
// Use PathAccessors with custom validators
const emailRule = customRules(o.user.email)
.isString()
.checkEmail("/api/check-email");
const usernameRule = customRules(o.user.username)
.isString()
.isValidUsername(5);
// Mix with regular string paths
const newsletterRule = customRules("user.preferences.newsletter")
.asBoolean()
.isDefined();
const { isValid, getMessages } = transval(emailRule, usernameRule, newsletterRule);
if (await isValid(formData)) {
console.log('Form data is valid!');
// Access individual rule data using PathAccessors
console.log('Email data:', emailRule.getField(o.user.email));
console.log('Username data:', usernameRule.getField(o.user.username));
console.log('Newsletter data:', newsletterRule.getField("user.preferences.newsletter"));
} else {
// Get validation messages using PathAccessors
const emailErrors = getMessages(o.user.email);
const usernameErrors = getMessages(o.user.username);
console.log('Validation errors:', {
email: emailErrors,
username: usernameErrors
});
}Individual Rule Chain Methods
Each rule chain created with rule() has its own methods for validation and data access. These methods work with both string paths and PathAccessors:
import { access } from 'defuss-transval';
type PersonData = {
person: {
age: number;
name: string;
};
};
const formData: PersonData = {
person: {
age: 25,
name: 'John Doe'
}
};
const o = access<PersonData>();
// Create rule with PathAccessor
const ageRule = rule(o.person.age).asNumber().isGreaterThan(18);
// Execute validation for this specific rule
const isValid = await ageRule.isValid(formData);
// Get validation messages for this rule
const messages = ageRule.getMessages();
// Get the entire transformed form data (includes all fields)
const allData = ageRule.getData();
// Get a specific value using PathAccessor
const specificAge = ageRule.getField(o.person.age);
// Or using string path
const specificName = ageRule.getField("person.name");
console.log('Age value:', specificAge); // Type-safe accessNote: getData() returns the entire form data object with transformations applied, while getField(path) returns the value at a specific field path.
Message Formatting
You can customize error message formatting by passing a formatter function to getMessages(). The formatter can return JSX elements for rich UI rendering:
import { FieldValidationMessage } from 'defuss-transval';
const emailRule = rule("email").isString().isEmail();
const { isValid, getMessages } = transval(emailRule);
if (!await isValid({ email: "invalid-email" })) {
// Get messages with custom JSX formatter
const formattedMessages = getMessages(undefined, (messages: FieldValidationMessage[]) => {
return (
<div className="error-container">
<h4>Email Validation Failed</h4>
<ul>
{messages.map((msg, index) => (
<li key={index} className="error-item">
<span className="error-icon">⚠️</span>
{msg.message}
</li>
))}
</ul>
</div>
);
});
console.log(formattedMessages); // JSX element
}
// Or for specific field formatting
const fieldErrors = getMessages("email", (messages: FieldValidationMessage[]) => {
return (
<div className="field-error">
{messages.map((msg, index) => (
<p key={index} className="error-text">{msg.message}</p>
))}
</div>
);
});Custom Message Formatting with useFormatter()
You can also apply custom formatting at the rule level using useFormatter():
const emailRule = rule("email")
.isString()
.isEmail()
.useFormatter((messages, format) => `❌ Email Error: ${format(messages)}`);
const { isValid } = transval(emailRule);
if (!await isValid({ email: "invalid" })) {
const messages = emailRule.getMessages();
console.log(messages[0].message); // "❌ Email Error: Invalid email format"
}Data Access Methods
After validation, you can access the original and transformed data using getData() and getField() methods:
import { rule, transval, access } from 'defuss-transval';
interface FormData {
name: string;
age: string; // Input as string
email: string;
preferences: {
newsletter: string; // "true" or "false"
};
}
const o = access<FormData>();
const { isValid, getMessages, getData, getField } = transval(
rule(o.name).isString(),
rule(o.age).isString().asNumber(), // First validate as string, then transform to number
rule(o.email).isEmail(),
rule(o.preferences.newsletter).isString().asBoolean() // Transform to boolean
);
const formData: FormData = {
name: "John Doe",
age: "25", // String input
email: "[email protected]",
preferences: {
newsletter: "true" // String input
}
};
if (await isValid(formData)) {
// Get the entire transformed data object
const transformedData = getData();
console.log(transformedData.age); // 25 (number, not string)
console.log(transformedData.preferences.newsletter); // true (boolean, not string)
// Get specific field values using string paths
const userAge = getField("age"); // 25 (transformed to number)
const newsletter = getField("preferences.newsletter"); // true (transformed to boolean)
// Get specific field values using PathAccessor (type-safe)
const userName = getField(o.name); // "John Doe" (original string)
const userEmail = getField(o.email); // "[email protected]"
}The getData() method returns the complete transformed data object, while getField(path) allows you to access specific fields. Both methods return undefined if called before validation or if the requested field doesn't exist.
Sync Validation
All validation operations are asynchronous and return Promises, but you can still use a callback-based approach if needed:
// Multiple rules validation
const { isValid } = transval(rule1, rule2, rule3);
// With callback support without await
isValid(formData, (isValidResult, error) => {
if (error) {
console.error('Validation error:', error);
} else {
console.log('Validation result:', isValidResult);
}
});Validation Options
You can configure validation behavior by passing options to the rule() function:
const options = {
timeout: 10000, // Validation timeout in milliseconds (default: 5000)
onValidationError: (error, step) => {
console.error('Validation step failed:', step, error);
}
};
const rule1 = rule("email", options).isString().isEmail();
// Timeout example - validation will throw if it takes longer than specified
try {
const isValid = await rule1.isValid(formData);
} catch (error) {
if (error.message.includes('timeout')) {
console.log('Validation timed out');
}
}Complete API Reference
Main Functions
rule(fieldPath, options?)- Create a validation rule for a specific field path (supports both string paths and PathAccessor objects)transval(...rules)- Combine multiple rules into a validation object that returns{ isValid, getMessages }rule.extend(CustomClass)- Extend the rule function with custom validatorsaccess<T>()- Create a PathAccessor for type-safe field paths
Validation Object Methods (from transval())
isValid(formData, callback?)- Execute all rules and return combined resultgetMessages(path?, formatter?)- Get validation messages asFieldValidationMessage[]with optional custom formatter that can return JSX (all messages or for specific field, supports both string paths and PathAccessor objects)getData()- Get the transformed data after validation (returns the merged transformed data from all validation chains)getField(path)- Get a specific field value from the transformed data (supports both string paths and PathAccessor objects)
Rule Chain Methods
isValid(formData, callback?)- Execute validation and return boolean resultgetMessages()- Get validation error messages for this rule asFieldValidationMessage[]getData()- Get the entire transformed form data objectgetField(path)- Get a specific value from the transformed data (supports both string paths and PathAccessor objects)
Message Format
All validation messages are returned as FieldValidationMessage[] objects with the following structure:
interface FieldValidationMessage {
message: string; // The validation error message
path: string; // The field path where the error occurred
}Available Validators
Type Validators
isSafeNumber()- Checks if a value is a safe number (finite and not NaN)isString()- Checks if a value is a stringisArray()- Checks if a value is an arrayisObject()- Checks if a value is an objectisDate()- Checks if a value is a valid dateisSafeNumeric()- Checks if a value is safely numericisInteger()- Checks if a value is an integer
Presence Validators
isNull()- Checks if a value is nullisRequired()- Checks if a value is not undefined, null, or emptyisUndefined()- Checks if a value is undefinedisDefined()- Checks if a value is defined (not undefined)isEmpty()- Checks if a value is empty (empty string, array, or object)
Format Validators
isEmail()- Checks if a value is a valid email addressisUrl()- Checks if a value is a valid URLisUrlPath()- Checks if a value is a valid URL pathisSlug()- Checks if a value is a valid slugisPhoneNumber()- Checks if a value is a valid phone number
Comparison Validators
is(value)- Strict equality check (===)isEqual(value)- Deep equality check using JSON comparisonisOneOf(allowedValues)- Checks if a value is one of the allowed values
Length Validators
isLongerThan(minLength, inclusive?)- Checks if length is longer than specifiedisShorterThan(maxLength, inclusive?)- Checks if length is shorter than specified
Numeric Comparison Validators
isGreaterThan(minValue, inclusive?)- Checks if a number is greater than specified valueisLessThan(maxValue, inclusive?)- Checks if a number is less than specified value
Date Comparison Validators
isAfter(minDate, inclusive?)- Checks if a date is after the specified dateisBefore(maxDate, inclusive?)- Checks if a date is before the specified date
Pattern Validator
hasPattern(pattern)- Checks if a string matches the specified regex pattern
Transformers
asString()- Transforms value to stringasNumber()- Transforms value to numberasBoolean()- Transforms value to booleanasDate()- Transforms value to Date objectasArray(transformerFn?)- Transforms value to arrayasInteger()- Transforms value to integer
Negation
All validators can be negated using the .not property:
rule("age").not.isLessThan(18) // age must NOT be less than 18
rule("email").not.isEmpty() // email must NOT be empty🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| npm build | Build a new version of the library. |
| npm test | Run the tests for the defuss-transval package. |
