Skip to main content

Validatable

The validatable.ts module is designed to extend objects with validation capabilities. It allows developers to define and enforce validation rules on object properties, ensuring data integrity and consistency.

Table of Contents

  1. Validation Overview
  2. Classes and Interfaces
  3. Common Validations
  4. Implementation Details
  5. More Usage Examples
  6. Advanced

Validatable - Runtime Data Model Validation

While using interfaces as data model definitions type-guides programmers during development, it does not support robust runtime enforcement on instantiated models. Ideally, data validation should accompany the data definition itself; however, native data modeling with Typescript interfaces often forces programmers to scatter validation logic elsewhere in the codebase.

An initializable class as the data model definition instead of using interface can assist greatly in pinpointing the source of bad data on runtime as well as keeping validation definitions nicely in the class definition itself. (** The only cavest is the performance overhead, which is relatively negligible unless you are dealing with millions of objects. See Performance Characteristics section for more details).

import { Validatable, defineOn } from 'ts-basis';
class MyModel {
strVal = 'test';
numVal = 5;
dateVal = null;
constructor(init?: Partial<MyModel>) {
defineOn( this, MyModel, lib => {
lib.validatable.enforce( this, { init }, {
strVal (value, e) { // throw on bad assignment
if ( typeof value !== 'string' || ! value.startsWith('test') ) {
return e.throw(`${e.path} must be a truthy string starting with 'test'`); }},
numVal (value, e) { // don't throw; just ignore bad assignments
if ( typeof v !== 'number' ) {
return e.cancel(); }}, // equivalently: return false;
dateVal (value, e) { // hijack & transform assigment value
if ( value === 'transformMe' ) {
return e.transformValue(new Date()); }},
});
});
}
}

// Generic object testing against a given type (true/false)
const result = Validatable.test({ strVal: 'bad' }, MyModel); // false
// Runtime Instantiation Guard
const inst1 = new MyModel(); // valid; default prop1='test' and prop2=5 are valid.
const inst2 = new MyModel({ strVal: 'test2' }) // valid; starts with 'test'
const inst3 = new MyModel({ strVal: 'yolo', numVal: 100 }); // throws; 'yolo' does not start with 'test'
// Runtime Property Assignment Guard
inst1.strVal = 'a'; // throws; and assignment cancels
inst1.numVal = 'string'; // canceled ("ignored"); will remain value 5
inst1.dateVal = 'transformMe'; // transformed; inst1.dateVal will be new Date() instance.
// External Data Casting
const inst1_1 = Validatable.cast(MyModel, { numVal: 10, dateVal: null }); // valid; new MyModel instance { strVal: 'test', numVal: 10, dateVal: null }
const inst1_2 = Validatable.cast(MyModel, { strVal: 'bad' }); // throws
const inst1_3 = Validatable.cast(MyModel, { strVal: 'bad' }, false); // returns null; throwError=false

Classes and Interfaces

ValidatableSettings

The ValidatableSettings class extends PropertiesControllerSettings and is responsible for initializing and storing settings related to validation.

export class ValidatableSettings extends PropertiesControllerSettings {
static extensionValidatable = 'Validatable';
extensionValidatable = ValidatableSettings.extensionValidatable;

constructor(init?: Partial<ValidatableSettings>) {
super(init);
if (init) {
Object.assign(this, init);
}
}
}
  • Properties:
    • extensionValidatable: A static and instance property used to identify the validatable extension.

ValidatableOptions

The ValidatableOptions interface defines the options available for configuring validation behavior.

export interface ValidatableOptions {
init?: any;
throwOnValidationError?: boolean; // default true
trackErrors?: boolean;
prepend?: boolean;
}
  • Properties:
    • init: Initial data for validation.
    • throwOnValidationError: Determines whether to throw an error on validation failure.
    • trackErrors: Indicates if errors should be tracked.
    • prepend: Option to prepend validation.

ValidatableExtensionData

The ValidatableExtensionData class implements TypeToolsExtensionData and stores metadata related to validation.

export class ValidatableExtensionData implements TypeToolsExtensionData {
options?: ValidatableOptions;
errors?: PropertyAccessTrace[];
cancels?: PropertyAccessTrace[];
}
  • Properties:
    • options: Stores validation options.
    • errors: Tracks validation errors.
    • cancels: Tracks cancellations.

Validatable

The Validatable class is the core component providing methods to enforce, test, and manage validations on target objects.

export class Validatable implements TypeToolsExtension {
// Methods and static properties

static getExtensionData(target: any, settings = ValidatableSettings): ValidatableExtensionData {
// Implementation
}

static typeCheck(target: any, settings = ValidatableSettings): boolean {
// Implementation
}

// Other methods...

constructor(settings?: Partial<ValidatableSettings>) {
this.settings = settingsInitialize(ValidatableSettings, settings);
}
}
  • Key Static Methods:
    • getExtensionData: Retrieves extension data for a target.
    • typeCheck: Checks if a target can be validated.
    • implementOn: Applies validation capabilities to a target.
    • enforce: Enforces validation rules on a target object.
    • test: Tests data against a validation type.

Common Validations

The module defines a set of common validations through the CommonValidations object, providing shorthand methods for various validation rules.

export const CommonValidations = {
notNull: 'notNull' as const,
boolean: 'boolean' as const,
number: 'number' as const,
string: 'string' as const,
object: 'object' as const,
array: 'array' as const,
// More validations...
}
  • Examples:
    • notNull: Ensures the value is neither null nor undefined.
    • boolean: Checks if the value is a boolean.
    • number: Ensures the value is of type number.

Implementation Details

The module leverages several utility functions and methods from other modules like type-tools, data-importable, properties-controller, etc. It integrates seamlessly with these components to provide a comprehensive validation system.

More Usage Examples

Here are some examples to illustrate how to use the Validatable class and its methods:

// Define a class
class User {
name: string;
age: number;
}

// Enforce validation rules
Validatable.enforce(User, {
init: { name: 'John', age: 30 },
throwOnValidationError: true,
}, {
name: CommonValidations.string,
age: CommonValidations.number,
});

// Create a user instance and validate
const user = new User();
user.name = 'Alice';
user.age = 25;

const isValid = Validatable.test(user, User);
console.log(`Is valid: ${isValid}`); // Output: Is valid: true

Advanced

Validatable Performance Characteristics

Baseline (on ~2.5 Ghz core, slower as more complex validations added):

  • Good data:
    • Instantiation: 100k/s (e.g. let a = new MyModel(data);)
    • Property set: 2.5M/s (e.g. a.prop = b)
  • Bad data:
    • If using try/catch block (expensive):
      • Instantiation: 50k/s, Property set: 250k/s
    • Using Validatable.errorsOf to detect fault (See next section)
      • Instantiation: 100k/s, Property set: 1.5M/s
  • TypeTools.test(obj, MyModel) 1 M/s (good data), 500k/s (bad data)

If you've called TypeTools.config.disableExtensions(Validatable) and are manually validating, instantiation and property set are both within around 5M ~ 50M/s

Performance Optimization

The slowest part of validatable class is the instantiation (due to registration overhead of TS Basis extensions) and try/catch block (creating new error and throwing is pretty expensive because of stack tracing overhead.)

Performance can be greatly helped by:

  1. If validity checking is all you need, use Validatable.test(obj, MyModel)
  2. Running with TypeTools.config.disableThrow() and manually checking
import { defineOn, TypeTools, Validatable, ... } from 'ts-basis';

const obj = { strVal: 'test', numVal: 100 };

// 1) If validity checking is all you need,
let valid: boolean;
valid = Validatable.test(obj, MyModel); // relatively inexpensive.
valid = TypeTools.test(obj, MyModel); // TypeTools.test is an alias of Validatable.test

// 2) Opt in for manually checking errors instead of throwing.
TypeTools.config.disableThrow();
const inst = new MyModel(obj); // would throw normally but doesn't throw.
inst.strVal = 'invalid'; // would throw normally but doesn't throw.
inst.numVal = 'not a number'; // cancels assignment.

valid = Validatable.resultOf(a); // false; inst has 2 errors and 1 cancel.

const instErrors = Validatable.errorsOf(a);
if (instErrors.length > 0) { /* some properties called e.throw */
for (const tracer of instErrors) {
console.log(`${tracer.e.path} has errored with ${tracer.trace.message}, stack: ${tracer.trace.stack}`);
// [0] = strVal ERROR at `new MyModel(obj)` where strVal = 'test' is executed.
// [1] = strVal ERROR at `inst.strVal = 'invalid';`
}
}
const instCancels = Validatable.cancelsOf(a);
if (aCancels.length > 0) { /* some properties called e.cancel */
for (const tracer of instCancels) {
console.log(`${tracer.e.path} has errored with ${tracer.trace.message}, stack: ${tracer.trace.stack}`);
// [0] = numVal assignment CANCEL on instantiation at `inst.numVal = 'not a number'`
}
}