Delta-Pack - v0.2.9
    Preparing search index...

    Delta-Pack - v0.2.9

    Delta-Pack TypeScript

    TypeScript implementation of delta-pack, a compact binary serialization format with efficient delta compression for real-time state synchronization.

    npm install @hpx7/delta-pack
    

    Delta-pack provides three approaches for working with schemas:

    1. Interpreter Mode - Runtime schema parsing with dynamic API
    2. Decorator Mode - Use TypeScript decorators for class-based schemas (builds on interpreter mode)
    3. Codegen Mode - Generate TypeScript code from schemas for compile-time type safety
    import { ObjectType, StringType, IntType, load, Infer } from "@hpx7/delta-pack";

    // Define schema type directly
    const Player = ObjectType("Player", {
    id: StringType(),
    name: StringType(),
    score: IntType(),
    });

    // Infer TypeScript type
    type Player = Infer<typeof Player>;
    // Result: { id: string; name: string; score: number }

    // Load API for the type
    const PlayerApi = load(Player);

    // Use the API
    const player = { id: "p1", name: "Alice", score: 100 };
    const encoded = PlayerApi.encode(player);
    const decoded = PlayerApi.decode(encoded);
    import { StringType, IntType, loadClass } from "@hpx7/delta-pack";

    class Player {
    @StringType()
    id: string = "";

    @StringType()
    name: string = "";

    @IntType()
    score: number = 0;
    }

    // Load API from decorated class
    const PlayerApi = loadClass(Player);

    // Use the API
    const player = new Player();
    player.id = "p1";
    player.name = "Alice";
    player.score = 100;

    const encoded = PlayerApi.encode(player);
    const decoded = PlayerApi.decode(encoded);

    Create a YAML schema file (schema.yml):

    # yaml-language-server: $schema=https://raw.githubusercontent.com/hpx7/delta-pack/refs/heads/main/schema.json

    Player:
    id: string
    name: string
    score: int

    Generate TypeScript code using the CLI:

    delta-pack generate schema.yml -l typescript -o generated.ts
    

    Then use the generated code:

    import { Player } from "./generated";

    const player: Player = { id: "p1", name: "Alice", score: 100 };
    const encoded = Player.encode(player);
    const decoded = Player.decode(encoded);

    Schemas can be defined in two ways:

    1. YAML - Human-readable format, useful for defining schemas separately from code. Parse with parseSchemaYml() for interpreter mode.
    2. TypeScript - Define schemas using the type constructor functions. Works with both interpreter and codegen modes. Required for using the Infer<> type utility.

    Note: The Infer<> type utility only works with TypeScript-defined schemas, not YAML-parsed schemas, since it requires compile-time type information.

    Create a schema.yml file:

    # yaml-language-server: $schema=https://raw.githubusercontent.com/hpx7/delta-pack/refs/heads/main/schema.json

    # Enums (list of string literals)
    Team:
    - RED
    - BLUE

    # Objects
    Player:
    id: string
    name: string
    score: int
    team: Team
    position: Position?

    Position:
    x: float(precision=0.1)
    y: float(precision=0.1)

    # Union types (list of type references)
    # A union is a list where all items are defined type names
    MoveAction:
    x: int
    y: int

    AttackAction:
    targetId: string
    damage: uint # shorthand for int(min=0)

    GameAction:
    - MoveAction
    - AttackAction

    # Complex types
    GameState:
    players: <string, Player>
    actions: GameAction[]
    round: uint
    phase: string

    # Bounded integers
    BoundedExample:
    health: int(min=0, max=100) # validated in fromJson, encoded as unsigned
    level: int(min=1, max=99)
    temperature: int(min=-50, max=50)

    Parse YAML schemas with:

    import { parseSchemaYml } from "@hpx7/delta-pack";
    import { readFileSync } from "fs";

    const schemaYml = readFileSync("schema.yml", "utf8");
    const schema = parseSchemaYml(schemaYml);

    See the main README for complete schema syntax reference.

    Define schemas using the type constructor functions. Named types (ObjectType, EnumType, UnionType) take a required name as the first parameter:

    import {
    ObjectType,
    StringType,
    IntType,
    UIntType,
    FloatType,
    ArrayType,
    OptionalType,
    RecordType,
    EnumType,
    UnionType,
    ReferenceType,
    } from "@hpx7/delta-pack";

    // Enum type
    const Team = EnumType("Team", ["RED", "BLUE"]);

    // Object types - define in dependency order
    const Position = ObjectType("Position", {
    x: FloatType({ precision: 0.1 }),
    y: FloatType({ precision: 0.1 }),
    });

    const Player = ObjectType("Player", {
    id: StringType(),
    name: StringType(),
    score: IntType(),
    team: ReferenceType(Team),
    position: OptionalType(ReferenceType(Position)),
    });

    // Union type - options are direct type references
    const MoveAction = ObjectType("MoveAction", {
    x: IntType(),
    y: IntType(),
    });

    const AttackAction = ObjectType("AttackAction", {
    targetId: StringType(),
    damage: UIntType(),
    });

    const GameAction = UnionType("GameAction", [MoveAction, AttackAction]);

    // Using unions in other types
    const GameState = ObjectType("GameState", {
    players: RecordType(StringType(), ReferenceType(Player)),
    actions: ArrayType(ReferenceType(GameAction)),
    round: UIntType(),
    phase: StringType(),
    });

    Union value format: Union values use a flattened discriminated union format with _type:

    // Creating a union value
    const action: GameAction = { _type: "MoveAction", x: 10, y: 20 };

    // Type narrowing with discriminant
    if (action._type === "MoveAction") {
    console.log(action.x, action.y);
    }

    The interpreter mode provides a runtime API for working with schemas.

    import { ObjectType, StringType, IntType, load, Infer } from "@hpx7/delta-pack";

    // Define schema type
    const Player = ObjectType("Player", {
    id: StringType(),
    name: StringType(),
    score: IntType(),
    });

    // Infer TypeScript type
    type Player = Infer<typeof Player>;
    // Result: { id: string; name: string; score: number }

    // Load interpreter API
    const PlayerApi = load(Player);

    Every loaded type provides these methods:

    Validates and parses JSON data, throwing if invalid. Use this when parsing untrusted or untyped data:

    // Parse unvalidated JSON data
    const jsonData = JSON.parse(networkResponse);
    const player = Player.fromJson(jsonData);

    For most cases, prefer using TypeScript types directly:

    // Preferred: use TypeScript types for compile-time safety
    const player: Player = { id: "p1", name: "Alice", score: 100 };

    Converts an object to JSON-serializable format. Useful for serializing to JSON or sending over HTTP:

    const player: Player = { id: "p1", name: "Alice", score: 100 };
    const json = Player.toJson(player);
    const jsonString = JSON.stringify(json);

    Format notes:

    • Maps (RecordType) are converted to plain objects
    • Optional object properties with undefined values are excluded from the JSON
    • Unions are converted to protobuf format: { TypeName: {...} }

    Example with unions:

    const action: GameAction = { _type: "MoveAction", x: 10, y: 20 };
    const json = GameAction.toJson(action);
    // Result: { MoveAction: { x: 10, y: 20 } }

    The JSON output uses protobuf format ({ TypeName: {...} }) which can be parsed back with fromJson().

    Serializes an object to binary format:

    const player = { id: "p1", name: "Alice", score: 100 };
    const bytes = Player.encode(player);
    console.log(`Encoded size: ${bytes.length} bytes`);

    Deserializes binary data back to an object:

    const decoded = Player.decode(bytes);
    // decoded = { id: 'p1', name: 'Alice', score: 100 }

    Encodes only the differences between two objects:

    const oldPlayer = { id: "p1", name: "Alice", score: 100 };
    const newPlayer = { id: "p1", name: "Alice", score: 150 };

    const diff = Player.encodeDiff(oldPlayer, newPlayer);
    console.log(`Diff size: ${diff.length} bytes`); // Much smaller!

    Applies a diff to reconstruct the new object:

    const reconstructed = Player.decodeDiff(oldPlayer, diff);
    // reconstructed = { id: 'p1', name: 'Alice', score: 150 }

    Deep equality comparison with appropriate tolerance for floats:

    const isEqual = Player.equals(player1, player2);
    

    For quantized floats (with precision), equality uses quantized value comparison. For non-quantized floats, equality uses epsilon-based comparison (0.00001 tolerance).

    Creates a deep clone of an object:

    const player1 = { id: "p1", name: "Alice", score: 100 };
    const player2 = Player.clone(player1);

    // Modifying the clone doesn't affect the original
    player2.score = 200;
    console.log(player1.score); // 100
    console.log(player2.score); // 200

    Important notes:

    • Creates deep copies of all nested objects, arrays, and maps
    • Primitives (strings, numbers, booleans) are copied by value
    • Useful for creating modified copies without mutating the original state

    The decorator mode provides a class-based API using TypeScript decorators. Schemas are inferred from decorated classes at runtime.

    The same type functions used to build schemas (StringType(), IntType(), ArrayType(), etc.) also work as property decorators. See Type Reference for the complete list.

    Additional decorator-specific features:

    Function Description
    EnumType("Name", []) Define an enum type
    UnionType("Name", []) Define a union from a list of variant classes
    ReferenceType(ref) Reference a class, enum, or union

    Referencing classes, enums, and unions:

    • Use ReferenceType(ClassName) to reference another class
    • Use EnumType("Name", [...]) to define an enum, then ReferenceType(EnumDef) to use it
    • Use UnionType("Name", [ClassA, ClassB]) to define a union, then ReferenceType(UnionDef) to use it
    import {
    StringType,
    IntType,
    UIntType,
    FloatType,
    BooleanType,
    ReferenceType,
    ArrayType,
    RecordType,
    OptionalType,
    EnumType,
    UnionType,
    Infer,
    loadClass,
    } from "@hpx7/delta-pack";

    // Enum type
    const Team = EnumType("Team", ["RED", "BLUE"]);
    type Team = Infer<typeof Team>;

    // Nested object
    class Position {
    @FloatType({ precision: 0.1 }) x: number = 0;
    @FloatType({ precision: 0.1 }) y: number = 0;
    }

    // Union types - define variant classes, then combine with UnionType
    class MoveAction {
    @IntType() x: number = 0;
    @IntType() y: number = 0;
    }

    class AttackAction {
    @StringType() targetId: string = "";
    @UIntType() damage: number = 0;
    }

    // Define union type - combines variant classes
    const GameAction = UnionType("GameAction", [MoveAction, AttackAction]);
    type GameAction = Infer<typeof GameAction>;

    // Main class
    class Player {
    @StringType() id: string = "";
    @StringType() name: string = "";
    @IntType() score: number = 0;
    @BooleanType() isOnline: boolean = false;

    @ReferenceType(Position) position: Position = new Position();
    @ReferenceType(Team) team: Team = Team.RED;

    @ArrayType(StringType()) tags: string[] = [];
    @ArrayType(ReferenceType(Position)) waypoints: Position[] = [];
    @RecordType(StringType(), IntType()) inventory: Map<string, number> = new Map();

    @OptionalType(StringType()) nickname?: string;
    @OptionalType(ReferenceType(GameAction)) lastAction?: GameAction;
    }

    // Load API and use
    const PlayerApi = loadClass(Player);
    const player = new Player();
    player.name = "Alice";
    const encoded = PlayerApi.encode(player);
    const decoded = PlayerApi.decode(encoded);

    Advanced patterns (nested containers, number modifiers):

    class AdvancedExample {
    @ArrayType(UIntType()) unsignedInts: number[] = [];
    @ArrayType(FloatType({ precision: 0.01 })) floats: number[] = [];
    @ArrayType(ArrayType(IntType())) matrix: number[][] = [];
    @RecordType(StringType(), ArrayType(IntType())) vectorsById: Map<string, number[]> = new Map();
    @OptionalType(ReferenceType(Player)) partner?: Player; // Self-reference
    }

    With the decorator API, union types are defined using UnionType() to combine variant classes:

    // Define variant classes (regular decorated classes)
    class JoinMessage {
    @StringType() name: string = "";
    }

    class InputMessage {
    @ReferenceType(ClientInput) input: ClientInput = new ClientInput();
    }

    // Define union type - combines variants
    const ClientMessage = UnionType("ClientMessage", [JoinMessage, InputMessage]);
    type ClientMessage = Infer<typeof ClientMessage>;

    // Load the API from the union
    const ClientMessageApi = loadClass(ClientMessage);

    Encoding: Pass variant class instances directly:

    const joinMsg = new JoinMessage();
    joinMsg.name = "Alice";
    const encoded = ClientMessageApi.encode(joinMsg);

    Decoding: Returns variant class instances. Use instanceof to narrow the type:

    const message = ClientMessageApi.decode(encoded);

    if (message instanceof JoinMessage) {
    console.log(message.name); // TypeScript knows this is JoinMessage
    } else if (message instanceof InputMessage) {
    console.log(message.input);
    }

    If you need access to the generated schema (e.g., for inspection or to use with load() directly), use buildSchema():

    import { buildSchema, load } from "@hpx7/delta-pack";

    const schema = buildSchema(Player);
    console.log(schema); // { Player: { type: "object", properties: { ... }, name: "Player" } }

    // Use with load() for more control
    const PlayerApi = load(schema["Player"]);

    tsconfig.json: Enable "experimentalDecorators": true

    Class requirements:

    1. Parameterless constructor (instantiable with new Class())
    2. Decorated properties must have default values (discovered via Object.keys(new Class()))
    3. At least one property must have a type decorator
    4. Unique class names (used as schema identifiers)

    Note: Properties without decorators are ignored (not serialized). Methods are also allowed. This lets you add helper methods or transient state to your classes.

    loadClass() returns the same API as load() in interpreter mode:

    Method Description
    encode(obj) Serialize to binary
    decode(bytes) Deserialize from binary
    encodeDiff(old, new) Encode delta between two states
    decodeDiff(old, diff) Apply delta to reconstruct new state
    equals(a, b) Deep equality comparison
    clone(obj) Deep clone
    toJson(obj) Convert to JSON-serializable format
    fromJson(obj) Validate and parse JSON data

    Key difference: In decorator mode, decode, decodeDiff, clone, and fromJson return proper class instances—not plain objects. This includes union variants (e.g., decoding a ClientMessage returns a JoinMessage or InputMessage instance).

    Aspect Schema API Decorator API
    Type constructors StringType(), ArrayType(), etc. Same - work as decorators too
    Object types ObjectType("Name", { ... }) Class with decorated properties
    Enums EnumType("Name", [...]) Same - EnumType("Name", [...])
    Unions UnionType("Name", [TypeA, TypeB]) UnionType("Name", [ClassA, ClassB])
    References Type references: ReferenceType(PlayerType) Class/enum/union refs: ReferenceType(ref)
    Loading load(RootType) loadClass(Class) or loadClass(UnionDef)
    Return types Plain objects Class instances with methods

    The codegen mode generates TypeScript code from schemas for compile-time type safety. Code generation is handled by the @hpx7/delta-pack-cli package.

    # Install CLI globally or use npx
    npm install -g @hpx7/delta-pack-cli

    # Generate from YAML schema
    delta-pack generate schema.yml -l typescript -o generated.ts

    For programmatic code generation, import from the CLI package:

    import { codegenTypescript } from "@hpx7/delta-pack-cli/codegen";
    import { parseSchemaYml } from "@hpx7/delta-pack";
    import { readFileSync, writeFileSync } from "fs";

    const schemaYml = readFileSync("schema.yml", "utf8");
    const schema = parseSchemaYml(schemaYml);
    const code = codegenTypescript(schema);
    writeFileSync("generated.ts", code);

    The generated code exports TypeScript types and runtime objects:

    import { Player, GameState } from "./generated";

    // TypeScript types are available
    const player: Player = {
    id: "p1",
    name: "Alice",
    score: 100,
    };

    // Runtime objects provide the same API as interpreter mode
    const encoded = Player.encode(player);
    const decoded = Player.decode(encoded);

    The generated code provides the same methods as interpreter mode:

    • Player.fromJson(obj) - Validate and parse JSON data
    • Player.toJson(obj) - Convert to JSON-serializable format
    • Player.encode(obj) - Serialize to binary
    • Player.decode(bytes) - Deserialize from binary
    • Player.encodeDiff(old, new) - Encode delta
    • Player.decodeDiff(old, diff) - Apply delta
    • Player.equals(a, b) - Deep equality
    • Player.clone(obj) - Deep clone object
    • Player.default() - Default instance

    Using Interpreter Mode:

    import {
    ObjectType,
    StringType,
    UIntType,
    FloatType,
    IntType,
    EnumType,
    ReferenceType,
    RecordType,
    load,
    Infer,
    } from "@hpx7/delta-pack";

    // Define schema types
    const Team = EnumType("Team", ["RED", "BLUE"]);

    const Position = ObjectType("Position", {
    x: FloatType(),
    y: FloatType(),
    });

    const Player = ObjectType("Player", {
    id: StringType(),
    username: StringType(),
    team: ReferenceType(Team),
    position: ReferenceType(Position),
    health: UIntType(),
    score: IntType(),
    });

    const GameState = ObjectType("GameState", {
    players: RecordType(StringType(), ReferenceType(Player)),
    round: UIntType(),
    timeRemaining: FloatType(),
    });

    // Infer types
    type GameStateType = Infer<typeof GameState>;

    // Load API
    const GameStateApi = load(GameState);

    // Create initial state
    const state1: GameStateType = {
    players: new Map([
    [
    "p1",
    {
    id: "p1",
    username: "Alice",
    team: "RED",
    position: { x: 100, y: 100 },
    health: 100,
    score: 0,
    },
    ],
    ]),
    round: 1,
    timeRemaining: 600.0,
    };

    // Updated state (player moved)
    const state2: GameStateType = {
    ...state1,
    players: new Map([["p1", { ...state1.players.get("p1")!, position: { x: 105, y: 102 } }]]),
    timeRemaining: 599.0,
    };

    // Delta encoding
    const diff = GameStateApi.encodeDiff(state1, state2);
    const reconstructed = GameStateApi.decodeDiff(state1, diff);

    Using Decorator Mode:

    import {
    StringType,
    UIntType,
    FloatType,
    IntType,
    ReferenceType,
    RecordType,
    EnumType,
    Infer,
    loadClass,
    } from "@hpx7/delta-pack";

    const Team = EnumType("Team", ["RED", "BLUE"]);
    type Team = Infer<typeof Team>;

    class Position {
    @FloatType() x: number = 0;
    @FloatType() y: number = 0;
    }

    class Player {
    @StringType() id: string = "";
    @StringType() username: string = "";
    @ReferenceType(Team) team: Team = "RED";
    @ReferenceType(Position) position: Position = new Position();
    @UIntType() health: number = 0;
    @IntType() score: number = 0;
    }

    class GameState {
    @RecordType(StringType(), ReferenceType(Player)) players: Map<string, Player> = new Map();
    @UIntType() round: number = 0;
    @FloatType() timeRemaining: number = 0;
    }

    // Load API
    const GameStateApi = loadClass(GameState);

    // Create initial state
    const state1 = new GameState();
    state1.round = 1;
    state1.timeRemaining = 600.0;

    const player = new Player();
    player.id = "p1";
    player.username = "Alice";
    player.team = "RED";
    player.position.x = 100;
    player.position.y = 100;
    player.health = 100;
    state1.players.set("p1", player);

    // Updated state (player moved)
    const state2 = GameStateApi.clone(state1);
    state2.players.get("p1")!.position.x = 105;
    state2.players.get("p1")!.position.y = 102;
    state2.timeRemaining = 599.0;

    // Delta encoding
    const diff = GameStateApi.encodeDiff(state1, state2);
    const reconstructed = GameStateApi.decodeDiff(state1, diff);

    Using Codegen Mode:

    Create a schema file (game.schema.yml):

    # yaml-language-server: $schema=https://raw.githubusercontent.com/hpx7/delta-pack/refs/heads/main/schema.json

    Team:
    - RED
    - BLUE

    Position:
    x: float
    y: float

    Player:
    id: string
    username: string
    team: Team
    position: Position
    health: uint
    score: int

    GameState:
    players: <string, Player>
    round: uint
    timeRemaining: float

    Generate the code:

    delta-pack generate game.schema.yml -l typescript -o generated.ts
    

    Then use the generated code:

    import { GameState, Player } from "./generated";

    // TypeScript types are available at compile time
    const state: GameState = GameState.default();
    state.players.set("p1", {
    id: "p1",
    username: "Alice",
    team: "RED",
    position: { x: 100, y: 100 },
    health: 100,
    score: 0,
    });

    // Same API as interpreter mode
    const encoded = GameState.encode(state);
    const decoded = GameState.decode(encoded);

    Delta encoding is most effective when:

    • State changes are incremental (only a few fields change per update)
    • You send updates frequently (e.g., 60 times per second in games)
    • Objects are medium to large (>50 bytes)

    Typical bandwidth savings:

    • Position-only updates: 90-95% smaller
    • Single field changes: 85-90% smaller
    • Multiple field changes: 70-85% smaller

    For maximum encodeDiff performance, use track() to automatically track which fields have changed. This allows delta encoding to skip comparison checks entirely. The tracking system uses version numbers, enabling efficient diffs from arbitrary baseline snapshots:

    import { track } from "@hpx7/delta-pack";

    // Wrap state with tracking
    const state = track({
    tick: 0,
    player: { x: 0, y: 0 },
    players: new Map([["p1", { x: 0, y: 0 }]]),
    });

    // Mutations are automatically tracked with version numbers
    state.tick = 1;
    state.player.x = 100;
    state.players.get("p1")!.x = 50;

    // Take a snapshot - captures current version
    const snapshot = GameStateApi.clone(state);

    // More mutations after snapshot
    state.tick = 2;
    state.player.y = 200;

    // Efficient delta encoding - only includes changes since snapshot
    const diff = GameStateApi.encodeDiff(snapshot, state);

    Deep tracking: Changes to nested objects, arrays, and maps automatically propagate up to parents. This works with:

    • Nested objects: state.player.x = 100 marks both player.x and state.player
    • Arrays: state.items[0].value = 99 marks the item, index 0, and state.items
    • Maps: state.players.get("p1")!.x = 50 marks the player, key "p1", and state.players
    • Array methods: push, pop, shift, unshift, splice all mark appropriate indices

    Multi-client game loop pattern:

    class Game {
    private state = track(GameStateApi.default());

    tick() {
    // Mutations are automatically tracked with versions
    this.state.tick++;
    for (const [id, input] of this.inputs) {
    const player = this.state.players.get(id)!;
    player.x += input.vx;
    player.y += input.vy;
    }
    }

    broadcast() {
    for (const client of this.clients) {
    // Each client has its own snapshot from their last update
    const diff = GameStateApi.encodeDiff(client.lastSnapshot, this.state);
    client.send(diff);
    // Clone captures version for next diff
    client.lastSnapshot = GameStateApi.clone(this.state);
    }
    // No manual clearing needed - version tracking handles it
    }
    }

    When to use tracking:

    • High-frequency updates (e.g., 20-60 times per second)
    • Large state objects where full comparison is expensive
    • Mutable state that changes incrementally each tick
    • Multiple clients with different baseline states (each gets minimal diff)

    Use precision for floats to reduce size:

    const Position = ObjectType("Position", {
    x: FloatType({ precision: 0.1 }), // ~10cm precision
    y: FloatType({ precision: 0.1 }),
    });

    This enables delta compression to skip encoding unchanged floats even if they differ slightly due to floating-point imprecision.

    Strings are automatically deduplicated within each encoding operation. Reuse common strings (player IDs, item names, etc.) to benefit from dictionary compression.

    • Use RecordType (maps) when entities have unique IDs
    • Use ArrayType when order matters or IDs aren't meaningful

    Maps enable efficient delta encoding for entity collections (only changed entities are encoded).

    See the examples/ directory for complete examples:

    • examples/Primitives/ - Basic primitive types
    • examples/User/ - User profile with unions and optionals
    • examples/GameState/ - Multiplayer game with complex state

    Each example includes:

    • schema.yml - Schema definition
    • state1.json, state2.json, ... - Example states demonstrating delta compression
    Function TypeScript Type Description
    StringType() string UTF-8 encoded string
    IntType() number Variable-length signed integer (zigzag)
    IntType({ min }) number Bounded integer, encoded as unsigned offset
    IntType({ min, max }) number Bounded integer with validation
    UIntType() number Shorthand for IntType({ min: 0 })
    FloatType() number 32-bit IEEE 754 float
    FloatType({ precision }) number Quantized float with specified precision
    BooleanType() boolean Single bit boolean

    Bounded integers: When min is specified, values are encoded as value - min using unsigned varints, which is more compact for values near the minimum. When both min and max are specified and the range fits in 8 bits (≤ 256 values), values are bit-packed using the minimum number of bits needed (ceil(log2(range))). The max parameter also adds validation in fromJson().

    Function TypeScript Type Description
    ArrayType(T) T[] Array of type T
    OptionalType(T) T | undefined Optional value of type T
    RecordType(K, V) Map<K, V> Map with key type K and value type V
    Function TypeScript Type Description
    ObjectType("Name", { ... }) { ... } Object with defined properties
    EnumType("Name", [...]) Union of string literals Enumerated string values
    UnionType("Name", [...]) { _type: string } & T Tagged union of named types
    ReferenceType(Type) Named type Reference to another named type
    SelfReferenceType() Self Reference to the containing type
    npm test              # Run all tests
    npm run test:coverage # Run with coverage report
    npm run test:ui # Open Vitest UI
    npm run typecheck
    
    npm run format        # Format code
    npm run format:check # Check formatting

    For detailed API documentation and schema syntax, see the main README.

    MIT