Skip to content

Custom Type Handlers

Learn to register custom type validation handlers with JSTC.

Creating Custom Handlers

Register custom type checkers:

typescript
import { JSTypeChecker } from "@briklab/lib/jstc";

const checker = new JSTypeChecker("boundary");

// Register a custom handler
checker.addCustomHandler("positiveNumber", (value) => {
  return typeof value === "number" && value > 0;
});

// Use the custom type
const result = checker.for([42]).check(["positiveNumber"]);
console.log(result); // true

const result2 = checker.for([-5]).check(["positiveNumber"]);
console.log(result2); // false

Live demo of custom handlers:

Console
No logs yet.

Multiple Custom Validators

Create specialized validators for domain logic:

typescript
import { JSTypeChecker } from "@briklab/lib/jstc";

const appChecker = new JSTypeChecker("hardened");

// Email validator
appChecker.addCustomHandler("email", (value) => {
  if (typeof value !== "string") return false;
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
});

// UUID validator
appChecker.addCustomHandler("uuid", (value) => {
  if (typeof value !== "string") return false;
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
  return uuidRegex.test(value);
});

// Integer validator
appChecker.addCustomHandler("integer", (value) => {
  return typeof value === "number" && Number.isInteger(value);
});

// Non-empty string validator
appChecker.addCustomHandler("nonEmptyString", (value) => {
  return typeof value === "string" && value.trim().length > 0;
});

// Usage
console.log(appChecker.for(["user@example.com"]).check(["email"])); // true
console.log(appChecker.for(["123e4567-e89b-12d3-a456-426614174000"]).check(["uuid"])); // true
console.log(appChecker.for([42]).check(["integer"])); // true
console.log(appChecker.for(["hello"]).check(["nonEmptyString"])); // true

Validating Objects with Custom Types

Check object properties:

typescript
import { JSTypeChecker } from "@briklab/lib/jstc";

const checker = new JSTypeChecker("hardened");

checker.addCustomHandler("email", (value) => {
  if (typeof value !== "string") return false;
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
});

interface User {
  id: number;
  email: string;
  active: boolean;
}

function validateUser(data: any): data is User {
  if (typeof data !== "object" || data === null) return false;

  return (
    checker.for([data.id]).check(["number"]) &&
    checker.for([data.email]).check(["email"]) &&
    checker.for([data.active]).check(["boolean"])
  );
}

const user = { id: 1, email: "john@example.com", active: true };
console.log(validateUser(user)); // true

Conditional Custom Handlers

Create context-dependent validators:

typescript
import { JSTypeChecker } from "@briklab/lib/jstc";

class ValidationContext {
  private checker: JSTypeChecker;

  constructor(hardenedMode: boolean = false) {
    this.checker = new JSTypeChecker(hardenedMode ? "hardened" : "boundary");
  }

  setupValidators(minPasswordLength: number = 8) {
    // Password validator with configurable length
    this.checker.addCustomHandler("password", (value) => {
      if (typeof value !== "string") return false;
      return (
        value.length >= minPasswordLength &&
        /[A-Z]/.test(value) &&
        /[a-z]/.test(value) &&
        /\d/.test(value)
      );
    });

    // Age validator
    this.checker.addCustomHandler("age", (value) => {
      return typeof value === "number" && value >= 18 && value <= 120;
    });

    // Phone validator (simplified)
    this.checker.addCustomHandler("phone", (value) => {
      if (typeof value !== "string") return false;
      return /^\d{10}$/.test(value.replace(/\D/g, ""));
    });
  }

  validate(values: unknown[], types: string[]): boolean {
    return this.checker.for(values).check(types);
  }
}

// Usage
const validator = new ValidationContext(true);
validator.setupValidators(12);
console.log(validator.validate(["MyPassword1"], ["password"])); // true
console.log(validator.validate([25], ["age"])); // true
console.log(validator.validate(["5551234567"], ["phone"])); // true

Composite Custom Types

Chain validators for complex validation:

typescript
import { JSTypeChecker } from "@briklab/lib/jstc";

class CompositeValidators {
  static setup(checker: JSTypeChecker) {
    checker.addCustomHandler("positiveInteger", (value) => {
      return typeof value === "number" && Number.isInteger(value) && value > 0;
    });

    checker.addCustomHandler("percentage", (value) => {
      return typeof value === "number" && value >= 0 && value <= 100;
    });

    checker.addCustomHandler("hexColor", (value) => {
      if (typeof value !== "string") return false;
      return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(value);
    });

    checker.addCustomHandler("url", (value) => {
      if (typeof value !== "string") return false;
      try {
        new URL(value);
        return true;
      } catch {
        return false;
      }
    });
  }
}

const checker = new JSTypeChecker("hardened");
CompositeValidators.setup(checker);

console.log(checker.for([42]).check(["positiveInteger"])); // true
console.log(checker.for([75]).check(["percentage"])); // true
console.log(checker.for(["#FF6B6B"]).check(["hexColor"])); // true
console.log(checker.for(["https://example.com"]).check(["url"])); // true

Pattern and Format Validation

Use custom handlers for format checks such as phone numbers, colors, and IP addresses.

Range and Bound Validation

Validate numeric ranges:

typescript
import { JSTypeChecker } from "@briklab/lib/jstc";

class BoundTypes {
  static checker = new JSTypeChecker("hardened");

  static isInRange(value: any, min: number, max: number): boolean {
    if (this.checker.for([value]).check(["number"]) !== true) return false;
    return value >= min && value <= max;
  }

  static isPercentage(value: any): boolean {
    return this.isInRange(value, 0, 100);
  }

  static isAge(value: any): boolean {
    return this.isInRange(value, 0, 150);
  }

  static isScaledValue(value: any, scale: number = 10): boolean {
    if (this.checker.for([value]).check(["number"]) !== true) return false;
    return Number.isInteger(value / scale);
  }

  static isWithinStandardDeviation(
    value: number,
    mean: number,
    stdDev: number,
    deviations: number = 2
  ): boolean {
    const lower = mean - stdDev * deviations;
    const upper = mean + stdDev * deviations;
    return this.isInRange(value, lower, upper);
  }
}

// Usage
console.log(BoundTypes.isInRange(50, 0, 100)); // true
console.log(BoundTypes.isPercentage(75)); // true
console.log(BoundTypes.isAge(25)); // true
console.log(BoundTypes.isAge(200)); // false

Collection Type Validation

Validate collection contents:

typescript
import { JSTypeChecker } from "@briklab/lib/jstc";

class CollectionTypes {
  static checker = new JSTypeChecker("hardened");

  static isArrayOf(arr: any, type: string | string[]): boolean {
    if (this.checker.for([arr]).check(["array"]) !== true) return false;
    return arr.every((item) => this.checker.for([item]).check([type]));
  }

  static isStringArray(arr: any): boolean {
    return this.isArrayOf(arr, "string");
  }

  static isNumberArray(arr: any): boolean {
    return this.isArrayOf(arr, "number");
  }

  static isMixed(arr: any): boolean {
    if (this.checker.for([arr]).check(["array"]) !== true) return false;
    return arr.some((item) => this.checker.for([item]).check(["string"])) &&
           arr.some((item) => this.checker.for([item]).check(["number"]));
  }

  static isObjectArray(arr: any): boolean {
    return this.isArrayOf(arr, "object");
  }

  static hasUniqueElements(arr: any[]): boolean {
    if (this.checker.for([arr]).check(["array"]) !== true) return false;
    return new Set(arr).size === arr.length;
  }
}

// Usage
console.log(CollectionTypes.isStringArray(["a", "b", "c"])); // true
console.log(CollectionTypes.isNumberArray([1, 2, 3])); // true
console.log(CollectionTypes.isMixed(["a", 1, "b", 2])); // true
console.log(CollectionTypes.hasUniqueElements([1, 2, 2, 3])); // false

Specialized Domain Types

Create domain-specific type checkers:

typescript
import { JSTypeChecker } from "@briklab/lib/jstc";

class DomainTypes {
  static checker = new JSTypeChecker("hardened");

  // E-commerce types
  static isSKU(value: any): boolean {
    if (this.checker.for([value]).check(["string"]) !== true) return false;
    return /^[A-Z0-9]{6,12}$/.test(value);
  }

  static isPrice(value: any): boolean {
    if (this.checker.for([value]).check(["number"]) !== true) return false;
    return value >= 0 && Number(value.toFixed(2)) === value;
  }

  // User types
  static isUsername(value: any): boolean {
    if (this.checker.for([value]).check(["string"]) !== true) return false;
    return /^[a-zA-Z0-9_]{3,20}$/.test(value);
  }

  static isTag(value: any): boolean {
    if (this.checker.for([value]).check(["string"]) !== true) return false;
    return /^#?[a-zA-Z0-9_-]{1,30}$/.test(value);
  }

  // URL types
  static isSlug(value: any): boolean {
    if (this.checker.for([value]).check(["string"]) !== true) return false;
    return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
  }

  static isAbsoluteURL(value: any): boolean {
    if (this.checker.for([value]).check(["string"]) !== true) return false;
    try {
      new URL(value);
      return true;
    } catch {
      return false;
    }
  }
}

// Usage
console.log(DomainTypes.isSKU("ABC123456")); // true
console.log(DomainTypes.isPrice(19.99)); // true
console.log(DomainTypes.isUsername("john_doe")); // true
console.log(DomainTypes.isSlug("my-slug")); // true

Composed Type Validators

Combine multiple type checks:

typescript
import { JSTypeChecker } from "@briklab/lib/jstc";

interface ValidationRule {
  type: string | string[];
  custom?: (value: any) => boolean;
}

class ComposedTypes {
  static checker = new JSTypeChecker("hardened");

  static validate(value: any, rules: ValidationRule[]): boolean {
    return rules.every((rule) => {
      // First check base type
      if (this.checker.for([value]).check([rule.type]) !== true) {
        return false;
      }

      // Then apply custom validator if provided
      if (rule.custom) {
        return rule.custom(value);
      }

      return true;
    });
  }

  static createValidator(rules: ValidationRule[]) {
    return (value: any) => this.validate(value, rules);
  }
}

// Usage
const emailValidator = ComposedTypes.createValidator([
  { type: "string" },
  {
    custom: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)
  }
]);

console.log(emailValidator("user@example.com")); // true
console.log(emailValidator("invalid")); // false

Conditional Custom Types

Type validations with conditions:

typescript
import { JSTypeChecker } from "@briklab/lib/jstc";

class ConditionalTypes {
  static checker = new JSTypeChecker();

  static isPasswordField(value: any, options?: { minLength: number }): boolean {
    if (this.checker.for([value]).check(["string"]) !== true) return false;

    const minLength = options?.minLength || 8;

    return (
      value.length >= minLength &&
      /[A-Z]/.test(value) &&
      /[a-z]/.test(value) &&
      /\d/.test(value) &&
      /[!@#$%^&*]/.test(value)
    );
  }

  static isOptionalField(value: any, type: string, required: boolean = false): boolean {
    if (!required && (value === null || value === undefined)) {
      return true;
    }
    return this.checker.for([value]).check([type]) === true;
  }

  static isConditionalField(
    value: any,
    primaryType: string,
    condition: (v: any) => boolean
  ): boolean {
    if (this.checker.for([value]).check([primaryType]) !== true) return false;
    return condition(value);
  }
}

// Usage
const strongPassword = "MyPassword123!";
console.log(ConditionalTypes.isPasswordField(strongPassword)); // true

console.log(ConditionalTypes.isOptionalField(undefined, "string", false)); // true
console.log(ConditionalTypes.isOptionalField(undefined, "string", true)); // false

Next Steps