CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-runtypes

Runtime validation for static types

Pending
Overview
Eval results
Files

contracts.mddocs/

Function Contracts

Runtime validation for function arguments and return values. Contracts ensure type safety at function boundaries, making APIs more robust and providing clear runtime error messages.

Capabilities

Contract

Creates function contracts for runtime argument and return value validation.

/**
 * Creates a function contract with optional argument and return validation
 * @param options - Contract configuration
 * @example Contract({ receives: Tuple(String, Number), returns: Boolean })
 */
function Contract<O extends ContractOptions>(options: O): ContractType<O>;

interface ContractOptions {
  receives?: Runtype<readonly unknown[]>;
  returns?: Runtype;
}

interface ContractType<O> {
  receives?: O["receives"];
  returns?: O["returns"];
  enforce<F extends Function>(fn: F): EnforcedFunction<O, F>;
}

Usage Examples:

import { Contract, String, Number, Boolean, Tuple } from "runtypes";

// Basic function contract
const AddContract = Contract({
  receives: Tuple(Number, Number),
  returns: Number
});

// Wrap function with contract
const safeAdd = AddContract.enforce((a: number, b: number) => a + b);

// Usage - validates arguments and return value
const result = safeAdd(5, 3); // 8
safeAdd("5", 3); // throws ValidationError - invalid arguments

// String processing contract
const StringProcessContract = Contract({
  receives: Tuple(String),
  returns: String
});

const processString = StringProcessContract.enforce((s: string) => s.toUpperCase());
const processed = processString("hello"); // "HELLO"

Argument Validation Only

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

// Validate only arguments
const LogContract = Contract({
  receives: Tuple(String, String) // level, message
});

const log = LogContract.enforce((level: string, message: string) => {
  console.log(`[${level}] ${message}`);
});

log("INFO", "Application started"); // OK
log("INFO"); // throws ValidationError - missing argument

// Variable argument validation
const SumContract = Contract({
  receives: Array(Number)
});

const sum = SumContract.enforce((...numbers: number[]) => 
  numbers.reduce((acc, n) => acc + n, 0)
);

const total = sum(1, 2, 3, 4, 5); // 15

Return Value Validation Only

import { Contract, String, Number, Object } from "runtypes";

// Validate only return value
const UserFactoryContract = Contract({
  returns: Object({
    id: Number,
    name: String,
    email: String
  })
});

const createUser = UserFactoryContract.enforce(() => ({
  id: 1,
  name: "Alice",
  email: "alice@example.com"
}));

const user = createUser(); // validated return value

// Contract catches bugs in implementation
const buggyCreateUser = UserFactoryContract.enforce(() => ({
  id: "1", // wrong type - will throw ValidationError
  name: "Alice",
  email: "alice@example.com"
}));

AsyncContract

Creates contracts for async functions with Promise validation.

/**
 * Creates a contract for async functions
 * @param options - Async contract configuration
 * @example AsyncContract({ receives: Tuple(String), resolves: Object({...}) })
 */
function AsyncContract<O extends AsyncContractOptions>(options: O): AsyncContractType<O>;

interface AsyncContractOptions {
  receives?: Runtype<readonly unknown[]>;
  returns?: Runtype;
  resolves?: Runtype;
}

interface AsyncContractType<O> {
  receives?: O["receives"];
  returns?: O["returns"];
  resolves?: O["resolves"];
  enforce<F extends AsyncFunction>(fn: F): EnforcedAsyncFunction<O, F>;
}

Usage Examples:

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

// API call contract
const FetchUserContract = AsyncContract({
  receives: Tuple(Number), // user ID
  resolves: Object({       // resolved value validation
    id: Number,
    name: String,
    email: String
  })
});

const fetchUser = FetchUserContract.enforce(async (id: number) => {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  return data; // validated against resolves runtype
});

// Usage
const user = await fetchUser(123); // Promise<{id: number, name: string, email: string}>

// Database operation contract
const SaveUserContract = AsyncContract({
  receives: Tuple(Object({
    name: String,
    email: String
  })),
  resolves: Object({
    id: Number,
    name: String, 
    email: String,
    createdAt: String
  })
});

const saveUser = SaveUserContract.enforce(async (userData) => {
  // Database save operation
  const savedUser = await db.users.create(userData);
  return savedUser;
});

Advanced Contract Patterns

Method Contracts

import { Contract, String, Number, Object, Boolean } from "runtypes";

class UserService {
  private users: Map<number, any> = new Map();

  // Contract for instance methods
  getUserById = Contract({
    receives: Tuple(Number),
    returns: Object({
      id: Number,
      name: String,
      active: Boolean
    }).optional() // might return undefined
  }).enforce((id: number) => {
    return this.users.get(id);
  });

  createUser = Contract({
    receives: Tuple(Object({
      name: String,
      email: String
    })),
    returns: Object({
      id: Number,
      name: String,
      email: String,
      active: Boolean
    })
  }).enforce((userData) => {
    const id = this.users.size + 1;
    const user = { ...userData, id, active: true };
    this.users.set(id, user);
    return user;
  });
}

// Usage
const service = new UserService();
const user = service.createUser({ name: "Alice", email: "alice@example.com" });
const retrieved = service.getUserById(user.id);

API Endpoint Contracts

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

// Request/Response contracts for API endpoints
const CreatePostContract = AsyncContract({
  receives: Tuple(Object({
    title: String,
    content: String,
    authorId: Number
  })),
  resolves: Object({
    id: Number,
    title: String,
    content: String,
    authorId: Number,
    createdAt: String,
    status: Union(Literal("draft"), Literal("published"))
  })
});

const GetPostsContract = AsyncContract({
  receives: Tuple(Object({
    page: Number.optional(),
    limit: Number.optional(),
    authorId: Number.optional()
  }).optional()),
  resolves: Object({
    posts: Array(Object({
      id: Number,
      title: String,
      excerpt: String,
      authorId: Number,
      createdAt: String
    })),
    pagination: Object({
      page: Number,
      limit: Number,
      total: Number,
      hasMore: Boolean
    })
  })
});

// Implementation
const createPost = CreatePostContract.enforce(async (postData) => {
  // Implementation details...
  return await db.posts.create(postData);
});

const getPosts = GetPostsContract.enforce(async (options = {}) => {
  // Implementation details...
  return await db.posts.findMany(options);
});

Error Handling with Contracts

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

// Result pattern with contracts
const Result = <T, E>(success: Runtype<T>, error: Runtype<E>) => Union(
  Object({ success: Literal(true), data: success }),
  Object({ success: Literal(false), error: error })
);

const SafeDivisionContract = Contract({
  receives: Tuple(Number, Number),
  returns: Result(Number, String)
});

const safeDivision = SafeDivisionContract.enforce((a: number, b: number) => {
  if (b === 0) {
    return { success: false, error: "Division by zero" };
  }
  return { success: true, data: a / b };
});

// Usage
const result = safeDivision(10, 2);
if (result.success) {
  console.log("Result:", result.data); // 5
} else {
  console.error("Error:", result.error);
}

// Async error handling
const AsyncResultContract = AsyncContract({
  receives: Tuple(String),
  resolves: Result(Object({ id: Number, data: String }), String)
});

const fetchData = AsyncResultContract.enforce(async (url: string) => {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      return { success: false, error: `HTTP ${response.status}` };
    }
    const data = await response.json();
    return { success: true, data };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

Contract Composition

import { Contract, String, Number, Object } from "runtypes";

// Base contracts for reuse
const UserData = Object({
  name: String,
  email: String,
  age: Number
});

const UserWithId = UserData.extend({
  id: Number,
  createdAt: String
});

// Compose contracts from base types
const CreateUserContract = Contract({
  receives: Tuple(UserData),
  returns: UserWithId
});

const UpdateUserContract = Contract({
  receives: Tuple(Number, UserData.asPartial()),
  returns: UserWithId
});

const DeleteUserContract = Contract({
  receives: Tuple(Number),
  returns: Boolean
});

// Generic CRUD contract factory
const createCrudContracts = <T>(entityType: Runtype<T>, entityWithId: Runtype<T & {id: number}>) => ({
  create: Contract({
    receives: Tuple(entityType),
    returns: entityWithId
  }),
  
  update: Contract({
    receives: Tuple(Number, entityType.asPartial?.() || entityType),
    returns: entityWithId
  }),
  
  delete: Contract({
    receives: Tuple(Number),
    returns: Boolean
  }),
  
  getById: Contract({
    receives: Tuple(Number),
    returns: entityWithId.optional?.() || entityWithId
  })
});

// Usage
const userContracts = createCrudContracts(UserData, UserWithId);

Performance and Debugging

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

// Contract with debugging
const DebugContract = Contract({
  receives: Tuple(String, Number),
  returns: String
});

const debugFunction = DebugContract.enforce((name: string, count: number) => {
  console.log(`Function called with: ${name}, ${count}`);
  const result = `${name} called ${count} times`;
  console.log(`Function returning: ${result}`);
  return result;
});

// Conditional contracts (for development vs production)
const createConditionalContract = (options: ContractOptions) => {
  if (process.env.NODE_ENV === 'development') {
    return Contract(options);
  }
  // Return pass-through in production
  return {
    enforce: <F extends Function>(fn: F) => fn
  };
};

const OptimizedContract = createConditionalContract({
  receives: Tuple(String),
  returns: String
});

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