npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

defuss-transval

v1.3.0

Published

A fast, functional, reliable and small state (e.g. form) validation library that aims for correctness and simplicity.

Readme

Powerful Data Transformation and Validation

defuss-transval provides 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 access

Note: 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 validators
  • access<T>() - Create a PathAccessor for type-safe field paths

Validation Object Methods (from transval())

  • isValid(formData, callback?) - Execute all rules and return combined result
  • getMessages(path?, formatter?) - Get validation messages as FieldValidationMessage[] 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 result
  • getMessages() - Get validation error messages for this rule as FieldValidationMessage[]
  • getData() - Get the entire transformed form data object
  • getField(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 string
  • isArray() - Checks if a value is an array
  • isObject() - Checks if a value is an object
  • isDate() - Checks if a value is a valid date
  • isSafeNumeric() - Checks if a value is safely numeric
  • isInteger() - Checks if a value is an integer

Presence Validators

  • isNull() - Checks if a value is null
  • isRequired() - Checks if a value is not undefined, null, or empty
  • isUndefined() - Checks if a value is undefined
  • isDefined() - 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 address
  • isUrl() - Checks if a value is a valid URL
  • isUrlPath() - Checks if a value is a valid URL path
  • isSlug() - Checks if a value is a valid slug
  • isPhoneNumber() - Checks if a value is a valid phone number

Comparison Validators

  • is(value) - Strict equality check (===)
  • isEqual(value) - Deep equality check using JSON comparison
  • isOneOf(allowedValues) - Checks if a value is one of the allowed values

Length Validators

  • isLongerThan(minLength, inclusive?) - Checks if length is longer than specified
  • isShorterThan(maxLength, inclusive?) - Checks if length is shorter than specified

Numeric Comparison Validators

  • isGreaterThan(minValue, inclusive?) - Checks if a number is greater than specified value
  • isLessThan(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 date
  • isBefore(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 string
  • asNumber() - Transforms value to number
  • asBoolean() - Transforms value to boolean
  • asDate() - Transforms value to Date object
  • asArray(transformerFn?) - Transforms value to array
  • asInteger() - 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. |