CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-runtypes

Runtime validation for static types

Pending
Overview
Eval results
Files

union-intersect.mddocs/

Union and Intersection Types

Combines multiple runtypes to create flexible validation schemas. Union types validate values that match any of several alternatives, while intersection types require values to match all specified types.

Capabilities

Union

Validates values that match at least one of the provided alternative runtypes.

/**
 * Creates a validator that accepts values matching any of the provided alternatives
 * @param alternatives - Array of runtypes, value must match at least one
 * @example Union(String, Number).check("hello") // "hello"
 * @example Union(String, Number).check(42) // 42
 * @example Union(String, Number).check(true) // throws ValidationError
 */
function Union<T extends readonly Runtype[]>(...alternatives: T): UnionRuntype<T>;

interface UnionRuntype<T> extends Runtype<UnionType<T>> {
  tag: "union";
  alternatives: T;
  match<C extends Cases<T>>(...cases: C): Matcher<T, ReturnType<C[number]>>;
}

Usage Examples:

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

// Basic union types
const StringOrNumber = Union(String, Number);
const value1 = StringOrNumber.check("hello"); // "hello"
const value2 = StringOrNumber.check(42); // 42

// Multiple alternatives
const ID = Union(String, Number, BigInt);
const userId = ID.check("user_123"); // "user_123"
const postId = ID.check(456); // 456

// Literal unions (enum-like)
const Status = Union(
  Literal("pending"),
  Literal("approved"), 
  Literal("rejected"),
  Literal("cancelled")
);

type StatusType = Static<typeof Status>; // "pending" | "approved" | "rejected" | "cancelled"

// Nullable types using union
const OptionalString = Union(String, Literal(null));
const OptionalNumber = Union(Number, Literal(undefined));

// Or use built-in helpers
const NullableString = String.nullable(); // Union(String, Literal(null))
const UndefinedableNumber = Number.undefinedable(); // Union(Number, Literal(undefined))

Discriminated Unions

Create type-safe discriminated unions for complex data structures.

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

// Shape discriminated union
const Circle = Object({
  type: Literal("circle"),
  radius: Number,
  center: Object({ x: Number, y: Number })
});

const Rectangle = Object({
  type: Literal("rectangle"), 
  width: Number,
  height: Number,
  topLeft: Object({ x: Number, y: Number })
});

const Triangle = Object({
  type: Literal("triangle"),
  vertices: Array(Object({ x: Number, y: Number }))
});

const Shape = Union(Circle, Rectangle, Triangle);

type ShapeType = Static<typeof Shape>;
// { type: "circle", radius: number, center: { x: number, y: number } } |
// { type: "rectangle", width: number, height: number, topLeft: { x: number, y: number } } |
// { type: "triangle", vertices: { x: number, y: number }[] }

// Usage with type narrowing
function calculateArea(shape: unknown): number {
  const validShape = Shape.check(shape);
  
  switch (validShape.type) {
    case "circle":
      return Math.PI * validShape.radius ** 2;
    case "rectangle":
      return validShape.width * validShape.height;  
    case "triangle":
      // Complex triangle area calculation
      return 0; // simplified
  }
}

Pattern Matching with Union

Use the built-in pattern matching functionality for unions.

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

const Result = Union(
  Object({ type: Literal("success"), data: String }),
  Object({ type: Literal("error"), message: String, code: Number }),
  Object({ type: Literal("loading") })
);

// Pattern matching
const handleResult = match(
  when(Object({ type: Literal("success"), data: String }), 
       ({ data }) => `Success: ${data}`),
  when(Object({ type: Literal("error"), message: String, code: Number }), 
       ({ message, code }) => `Error ${code}: ${message}`),
  when(Object({ type: Literal("loading") }), 
       () => "Loading...")
);

// Usage
const response = handleResult({ type: "success", data: "Hello World" });
// "Success: Hello World"

Intersect

Validates values that match all of the provided runtypes simultaneously.

/**
 * Creates a validator that requires values to match all provided runtypes
 * @param intersectees - Array of runtypes, value must match all of them
 * @example Intersect(Object({name: String}), Object({age: Number})).check({name: "Alice", age: 25})
 */
function Intersect<T extends readonly Runtype[]>(...intersectees: T): IntersectRuntype<T>;

interface IntersectRuntype<T> extends Runtype<IntersectType<T>> {
  tag: "intersect";
  intersectees: T;
}

Usage Examples:

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

// Combining object types
const PersonBase = Object({
  name: String,
  age: Number
});

const ContactInfo = Object({
  email: String,
  phone: String.optional()
});

const PersonWithContact = Intersect(PersonBase, ContactInfo);

type PersonWithContactType = Static<typeof PersonWithContact>;
// {
//   name: string;
//   age: number;
//   email: string;
//   phone?: string;
// }

const person = PersonWithContact.check({
  name: "Alice",
  age: 25,
  email: "alice@example.com",
  phone: "555-0123"
});

Mixin Patterns with Intersect

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

// Base entity
const BaseEntity = Object({
  id: String,
  createdAt: Number,
  updatedAt: Number
});

// Audit trail
const Auditable = Object({
  createdBy: String,
  updatedBy: String,
  version: Number
});

// Soft delete capability  
const SoftDeletable = Object({
  deleted: Boolean,
  deletedAt: Number.optional(),
  deletedBy: String.optional()
});

// User entity with mixins
const User = Intersect(
  BaseEntity,
  Auditable,
  SoftDeletable,
  Object({
    name: String,
    email: String,
    role: Union(Literal("admin"), Literal("user"))
  })
);

type UserType = Static<typeof User>;
// Combines all properties from all intersected types

Advanced Union Patterns

Async Result Types

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

const AsyncResult = Union(
  Object({
    status: Literal("pending")
  }),
  Object({
    status: Literal("fulfilled"),
    value: Unknown
  }),
  Object({
    status: Literal("rejected"), 
    reason: String
  })
);

// Usage in async contexts
async function handleAsyncResult(result: unknown) {
  const validResult = AsyncResult.check(result);
  
  if (validResult.status === "fulfilled") {
    console.log("Success:", validResult.value);
  } else if (validResult.status === "rejected") {
    console.error("Error:", validResult.reason);
  } else {
    console.log("Still pending...");
  }
}

Conditional Types with Union

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

// API endpoint responses
const UserResponse = Object({
  type: Literal("user"),
  data: Object({
    id: Number,
    name: String,
    email: String
  })
});

const PostResponse = Object({
  type: Literal("post"),
  data: Object({
    id: Number,
    title: String,
    content: String,
    authorId: Number
  })
});

const ListResponse = Object({
  type: Literal("list"),
  data: Array(Unknown),
  pagination: Object({
    page: Number,
    total: Number,
    hasMore: Boolean
  })
});

const ApiResponse = Union(UserResponse, PostResponse, ListResponse);

// Type-safe response handling
function processResponse(response: unknown) {
  const validResponse = ApiResponse.check(response);
  
  switch (validResponse.type) {
    case "user":
      // TypeScript knows data is user object
      return `User: ${validResponse.data.name}`;
    case "post":
      // TypeScript knows data is post object  
      return `Post: ${validResponse.data.title}`;
    case "list":
      // TypeScript knows about pagination
      return `List: ${validResponse.data.length} items (page ${validResponse.pagination.page})`;
  }
}

Combining Union and Intersection

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

// Base types
const Identifiable = Object({ id: String });
const Timestamped = Object({ timestamp: Number });

// Different entity types
const User = Object({ type: Literal("user"), name: String, email: String });
const Post = Object({ type: Literal("post"), title: String, content: String });

// Combine with mixins using intersection, then union for alternatives
const TimestampedUser = Intersect(User, Identifiable, Timestamped);
const TimestampedPost = Intersect(Post, Identifiable, Timestamped);

const Entity = Union(TimestampedUser, TimestampedPost);

type EntityType = Static<typeof Entity>;
// ({ type: "user", name: string, email: string } & { id: string } & { timestamp: number }) |
// ({ type: "post", title: string, content: string } & { id: string } & { timestamp: number })

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