CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-runtypes

Runtime validation for static types

Pending
Overview
Eval results
Files

utilities.mddocs/

Utilities

Pattern matching and helper utilities for working with runtypes. These tools provide additional functionality for advanced validation scenarios and convenient type manipulation.

Capabilities

Pattern Matching

Pattern matching utilities for working with union types and discriminated unions.

/**
 * Creates a pattern matcher for union types
 * @param cases - Array of case handlers created with when()
 * @returns Function that matches value against cases
 * @example match(when(String, s => s.length), when(Number, n => n * 2))
 */
function match<C extends readonly Case[]>(...cases: C): Matcher<C>;

/**
 * Creates a case for pattern matching
 * @param runtype - Runtype to match against
 * @param transformer - Function to execute when matched
 * @returns Case object for use with match()
 * @example when(String, s => s.toUpperCase())
 */
function when<T, U>(runtype: Runtype<T>, transformer: (value: T) => U): Case<T, U>;

type Matcher<C> = (value: unknown) => ReturnType<C[number]>;
type Case<T, U> = [runtype: Runtype<T>, transformer: (value: T) => U];

Usage Examples:

import { match, when, String, Number, Boolean, Object, Union, Literal } from "runtypes";

// Basic pattern matching
const processValue = match(
  when(String, str => `String: ${str.toUpperCase()}`),
  when(Number, num => `Number: ${num * 2}`),
  when(Boolean, bool => `Boolean: ${bool ? "yes" : "no"}`)
);

console.log(processValue("hello")); // "String: HELLO"
console.log(processValue(42)); // "Number: 84"
console.log(processValue(true)); // "Boolean: yes"

// Discriminated union pattern matching
const Shape = Union(
  Object({ type: Literal("circle"), radius: Number }),
  Object({ type: Literal("rectangle"), width: Number, height: Number }),
  Object({ type: Literal("triangle"), base: Number, height: Number })
);

const calculateArea = match(
  when(Object({ type: Literal("circle"), radius: Number }), 
       ({ radius }) => Math.PI * radius ** 2),
  when(Object({ type: Literal("rectangle"), width: Number, height: Number }), 
       ({ width, height }) => width * height),
  when(Object({ type: Literal("triangle"), base: Number, height: Number }), 
       ({ base, height }) => (base * height) / 2)
);

const area = calculateArea({ type: "circle", radius: 5 }); // ~78.54

API Response Pattern Matching

import { match, when, Object, Literal, String, Number, Array } from "runtypes";

// API response types
const SuccessResponse = Object({
  status: Literal("success"),
  data: Unknown
});

const ErrorResponse = Object({
  status: Literal("error"),
  message: String,
  code: Number
});

const LoadingResponse = Object({
  status: Literal("loading"),
  progress: Number.optional()
});

// Pattern matcher for responses
const handleApiResponse = match(
  when(SuccessResponse, ({ data }) => ({
    type: "success" as const,
    payload: data
  })),
  when(ErrorResponse, ({ message, code }) => ({
    type: "error" as const,
    error: `${code}: ${message}`
  })),
  when(LoadingResponse, ({ progress = 0 }) => ({
    type: "loading" as const,
    progress
  }))
);

// Usage
const response1 = handleApiResponse({ status: "success", data: { users: [] } });
// { type: "success", payload: { users: [] } }

const response2 = handleApiResponse({ status: "error", message: "Not found", code: 404 });
// { type: "error", error: "404: Not found" }

Advanced Pattern Matching

import { match, when, Union, Object, String, Number, Array } from "runtypes";

// Complex data processing
const DataProcessor = match(
  // Process strings
  when(String, str => ({
    type: "text",
    length: str.length,
    words: str.split(" ").length,
    uppercase: str.toUpperCase()
  })),
  
  // Process numbers
  when(Number, num => ({
    type: "numeric",
    value: num,
    squared: num ** 2,
    isEven: num % 2 === 0
  })),
  
  // Process arrays
  when(Array(Unknown), arr => ({
    type: "list",
    count: arr.length,
    isEmpty: arr.length === 0,
    first: arr[0],
    last: arr[arr.length - 1]
  })),
  
  // Process objects
  when(Object({ name: String, age: Number }), person => ({
    type: "person",
    greeting: `Hello, ${person.name}!`,
    isAdult: person.age >= 18,
    category: person.age < 13 ? "child" : person.age < 20 ? "teen" : "adult"
  }))
);

// Usage examples
console.log(DataProcessor("Hello World")); 
// { type: "text", length: 11, words: 2, uppercase: "HELLO WORLD" }

console.log(DataProcessor(42));
// { type: "numeric", value: 42, squared: 1764, isEven: true }

console.log(DataProcessor([1, 2, 3]));
// { type: "list", count: 3, isEmpty: false, first: 1, last: 3 }

console.log(DataProcessor({ name: "Alice", age: 25 }));
// { type: "person", greeting: "Hello, Alice!", isAdult: true, category: "adult" }

Built-in Union Pattern Matching

Union types also provide their own pattern matching method.

import { Union, Object, Literal, String, Number } from "runtypes";

const Result = Union(
  Object({ type: Literal("ok"), value: String }),
  Object({ type: Literal("error"), message: String })
);

// Using Union's built-in match method
const processResult = Result.match(
  // Case for "ok" variant
  ({ value }) => `Success: ${value}`,
  // Case for "error" variant  
  ({ message }) => `Failed: ${message}`
);

const result1 = processResult({ type: "ok", value: "Hello" }); // "Success: Hello"
const result2 = processResult({ type: "error", message: "Oops" }); // "Failed: Oops"

Additional Utilities

Optional Helper

While Optional is primarily used in object definitions, it can be useful for creating optional validation chains.

import { Optional, String, Number } from "runtypes";

// Create optional validators
const OptionalString = Optional(String);
const OptionalNumber = Optional(Number, 0); // with default

// Check for optional presence
function processOptionalValue(value: unknown) {
  if (Optional.isOptional(OptionalString)) {
    console.log("This is an optional type");
  }
  
  // Optional validation allows undefined
  const result = OptionalString.underlying.guard(value);
  return result;
}

Lazy Evaluation

For recursive type definitions and forward references.

import { Lazy, Object, String, Number, Array, Union } from "runtypes";

// Recursive data structures
type TreeNodeType = {
  value: string;
  children: TreeNodeType[];
};

const TreeNode: Runtype<TreeNodeType> = Lazy(() => Object({
  value: String,
  children: Array(TreeNode) // Self-reference
}));

// Usage
const tree = TreeNode.check({
  value: "root",
  children: [
    {
      value: "child1", 
      children: [
        { value: "grandchild1", children: [] },
        { value: "grandchild2", children: [] }
      ]
    },
    { value: "child2", children: [] }
  ]
});

// Mutually recursive types
type PersonType = {
  name: string;
  company?: CompanyType;
};

type CompanyType = {
  name: string;
  employees: PersonType[];
};

const Person: Runtype<PersonType> = Lazy(() => Object({
  name: String,
  company: Company.optional()
}));

const Company: Runtype<CompanyType> = Lazy(() => Object({
  name: String,
  employees: Array(Person)
}));

InstanceOf Utility

Validate instanceof relationships with constructor functions.

import { InstanceOf, Object, String, Union } from "runtypes";

// Built-in constructors
const DateValidator = InstanceOf(Date);
const RegExpValidator = InstanceOf(RegExp);
const ErrorValidator = InstanceOf(Error);

const date = DateValidator.check(new Date()); // Date
const regex = RegExpValidator.check(/pattern/); // RegExp

// Custom classes
class User {
  constructor(public name: string, public age: number) {}
}

class Admin extends User {
  constructor(name: string, age: number, public permissions: string[]) {
    super(name, age);
  }
}

const UserValidator = InstanceOf(User);
const AdminValidator = InstanceOf(Admin);

const user = new User("Alice", 25);
const admin = new Admin("Bob", 30, ["read", "write"]);

UserValidator.check(user); // ✓ User instance
UserValidator.check(admin); // ✓ Admin extends User
AdminValidator.check(admin); // ✓ Admin instance
AdminValidator.check(user); // ✗ throws ValidationError

// Combined with other validators
const UserData = Union(
  InstanceOf(User),
  Object({ name: String, age: Number }) // Plain object alternative
);

Function Validation

Basic function validation (note: cannot validate function signatures at runtime).

import { Function as FunctionValidator } from "runtypes";

// Validate that something is a function
const fn = FunctionValidator.check(() => "hello"); // Function
const method = FunctionValidator.check(Math.max); // Function

// Combined with other types
const Callback = Union(FunctionValidator, Literal(null));

function processWithCallback(data: unknown, callback: unknown) {
  const validCallback = Callback.check(callback);
  
  if (typeof validCallback === "function") {
    return validCallback(data);
  }
  
  return data; // No callback provided
}

Spread Utility

Enable rest/spread syntax in tuple definitions.

import { Spread, Tuple, Array, String, Number } from "runtypes";

// Tuple with rest elements
const LogEntry = Tuple(
  String,           // timestamp
  String,           // level
  Spread(Array(String)) // variable message parts
);

const entry1 = LogEntry.check(["2024-01-15T10:30:00Z", "INFO", "User", "logged", "in"]);
const entry2 = LogEntry.check(["2024-01-15T10:31:00Z", "ERROR", "Database", "connection", "failed", "retry", "in", "5s"]);

// Mixed tuple with leading, rest, and trailing
const DataRecord = Tuple(
  Number,               // id
  String,               // name  
  Spread(Array(Number)), // scores (variable length)
  Boolean               // active
);

const record = DataRecord.check([1, "Alice", 95, 87, 92, 88, true]);
// id=1, name="Alice", scores=[95,87,92,88], active=true

Utility Composition

import { match, when, Union, Object, Literal, String, Number, Array } from "runtypes";

// Complex validation and processing pipeline
const DataInput = Union(
  String,
  Number,
  Array(String),
  Object({ type: Literal("user"), name: String, age: Number }),
  Object({ type: Literal("product"), name: String, price: Number })
);

const processInput = match(
  when(String, str => ({ kind: "text", processed: str.trim().toLowerCase() })),
  when(Number, num => ({ kind: "numeric", processed: num.toString() })),
  when(Array(String), arr => ({ kind: "list", processed: arr.join(", ") })),
  when(Object({ type: Literal("user"), name: String, age: Number }), 
       user => ({ kind: "user", processed: `${user.name} (${user.age})` })),
  when(Object({ type: Literal("product"), name: String, price: Number }), 
       product => ({ kind: "product", processed: `${product.name}: $${product.price}` }))
);

// Usage
const results = [
  processInput("  Hello World  "),
  processInput(42),
  processInput(["apple", "banana", "cherry"]),
  processInput({ type: "user", name: "Alice", age: 25 }),
  processInput({ type: "product", name: "Widget", price: 19.99 })
];

results.forEach(result => {
  console.log(`${result.kind}: ${result.processed}`);
});
// text: hello world
// numeric: 42
// list: apple, banana, cherry
// user: Alice (25)
// product: Widget: $19.99

Install with Tessl CLI

npx tessl i tessl/npm-runtypes

docs

composite.md

constraints.md

contracts.md

index.md

literals.md

primitives.md

results.md

templates.md

union-intersect.md

utilities.md

validation.md

tile.json