Skip to main content

TypeScript Patterns

Voltaire uses specific TypeScript patterns for type safety, tree-shaking, and API consistency.

Branded Types

All primitives are branded Uint8Arrays - zero runtime overhead, full type safety.
// AddressType.ts
declare const brand: unique symbol;

export type AddressType = Uint8Array & {
  readonly [brand]: "Address";
  readonly length: 20;
};

Why Branded Types?

  1. Type Safety: Can’t accidentally pass Hash where Address expected
  2. Zero Overhead: Just TypeScript - no runtime checks
  3. Uint8Array Base: Works with all binary APIs natively
  4. Self-Documenting: Types describe exact byte lengths
// Type system catches errors at compile time
function transfer(to: Address, amount: Uint256): void { ... }

const hash = Hash.fromHex("0x...");  // 32 bytes
transfer(hash, amount);  // ❌ Type error: Hash is not Address

File Organization

Each primitive follows this structure:
Address/
├── AddressType.ts         # Type definition only
├── from.js                # Constructor (no wrapper needed)
├── toHex.js               # Internal method
├── equals.js              # Internal method
├── isValid.js             # Validation
├── index.ts               # Dual exports + wrappers
└── Address.test.ts        # Tests

Why .js for Implementation?

Implementation files use .js with JSDoc types:
// toHex.js
/**
 * @param {import('./AddressType.js').AddressType} address
 * @returns {import('../Hex/HexType.js').Hex}
 */
export function toHex(address) {
  return Hex.fromBytes(address);
}
Benefits:
  • No compilation step for runtime code
  • JSDoc provides type checking
  • Better tree-shaking
  • Faster builds

Namespace Pattern

Functions are exported both internally and wrapped:
// index.ts

// Internal export (underscore prefix)
export { toHex as _toHex } from "./toHex.js";
export { equals as _equals } from "./equals.js";

// Public wrapper with auto-conversion
export function toHex(value: AddressInput): Hex {
  return _toHex(from(value));
}

export function equals(a: AddressInput, b: AddressInput): boolean {
  return _equals(from(a), from(b));
}

Usage

import * as Address from "@voltaire/primitives/Address";

// Public API - accepts various inputs
const hex = Address.toHex("0x742d35Cc6634C0532925a3b844Bc9e7595f251e3");

// Internal API - requires branded type, no conversion overhead
const addr = Address.from("0x742d35Cc6634C0532925a3b844Bc9e7595f251e3");
const hex2 = Address._toHex(addr);

Why Dual Exports?

ExportUse CasePerformance
toHex()General usageConversion overhead
_toHex()Hot paths, already have branded typeZero overhead

Constructor Pattern

Main Constructor

The primary constructor is just the type name:
// ✅ Preferred
const addr = Address("0x742d...");
const hash = Hash("0xabc...");
const uint = Uint256(42n);

// ❌ Avoid
const addr = Address.from("0x742d...");  // More verbose

Named Constructors

For specific input types:
// From specific formats
const addr1 = Address.fromHex("0x742d35Cc6634C0532925a3b844Bc9e7595f251e3");
const addr2 = Address.fromBytes(bytes);
const addr3 = Address.fromPublicKey(pubkey);

// To specific formats
const hex = Address.toHex(addr);
const bytes = Address.toBytes(addr);
const checksummed = Address.toChecksummed(addr);

Type Narrowing

Input Types

Define input types for flexible function signatures:
// types.ts
export type AddressInput =
  | AddressType           // Already branded
  | Hex                   // Hex string
  | Uint8Array            // Raw bytes
  | `0x${string}`;        // Literal hex

The from Function

Central converter that handles all inputs:
// from.js
export function from(value: AddressInput): AddressType {
  if (isAddress(value)) return value;          // Already branded
  if (typeof value === "string") return fromHex(value);
  if (value instanceof Uint8Array) return fromBytes(value);
  throw new Error(`Invalid address input: ${value}`);
}

Validation Pattern

Type Guards

// is.js
export function is(value: unknown): value is AddressType {
  return value instanceof Uint8Array &&
         value.length === 20 &&
         hasAddressBrand(value);
}

Validation Functions

// isValid.js
export function isValid(value: unknown): boolean {
  if (typeof value === "string") {
    return /^0x[0-9a-fA-F]{40}$/.test(value);
  }
  if (value instanceof Uint8Array) {
    return value.length === 20;
  }
  return false;
}

Error Handling

Custom Errors

// errors.ts
export class InvalidAddressError extends Error {
  readonly name = "InvalidAddressError";

  constructor(value: unknown) {
    super(`Invalid address: ${String(value).slice(0, 100)}`);
  }
}

Usage in Functions

export function fromHex(hex: string): AddressType {
  if (!/^0x[0-9a-fA-F]{40}$/.test(hex)) {
    throw new InvalidAddressError(hex);
  }
  // ... conversion logic
}

Testing Pattern

Tests in separate .test.ts files using Vitest:
// Address.test.ts
import { describe, it, expect } from "vitest";
import * as Address from "./index.js";

describe("Address", () => {
  describe("fromHex", () => {
    it("converts valid lowercase hex", () => {
      const addr = Address.fromHex("0x742d35cc6634c0532925a3b844bc9e7595f251e3");
      expect(addr).toBeInstanceOf(Uint8Array);
      expect(addr.length).toBe(20);
    });

    it("throws on invalid hex", () => {
      expect(() => Address.fromHex("0xinvalid")).toThrow();
    });
  });

  describe("toChecksummed", () => {
    it("applies EIP-55 checksum", () => {
      const addr = Address.fromHex("0x742d35cc6634c0532925a3b844bc9e7595f251e3");
      expect(Address.toChecksummed(addr)).toBe("0x742d35Cc6634C0532925a3b844Bc9e7595f251e3");
    });
  });
});

Index File Template

Complete index.ts structure:
// Re-export type
export type { AddressType } from "./AddressType.js";
export type { AddressType as Address } from "./AddressType.js";

// Constructor (no wrapper needed)
export { from } from "./from.js";
export { from as Address } from "./from.js";

// Named constructors
export { fromHex } from "./fromHex.js";
export { fromBytes } from "./fromBytes.js";

// Internal methods (underscore prefix)
export { toHex as _toHex } from "./toHex.js";
export { toBytes as _toBytes } from "./toBytes.js";
export { equals as _equals } from "./equals.js";

// Public wrappers
import { from } from "./from.js";
import { toHex as _toHex } from "./toHex.js";
import type { AddressInput } from "./types.js";

export function toHex(value: AddressInput): Hex {
  return _toHex(from(value));
}

// Validation (no wrapper needed - handles any input)
export { isValid } from "./isValid.js";
export { is } from "./is.js";

// Constants
export { ZERO_ADDRESS } from "./constants.js";

Common Mistakes

Avoid these patterns:
// ❌ Using .ts for implementations
// toHex.ts - requires compilation, worse tree-shaking

// ❌ Missing internal exports
export { toHex } from "./toHex.js";  // No _toHex variant

// ❌ Wrapper without from() call
export function toHex(value: AddressInput): Hex {
  return _toHex(value);  // Wrong - value might not be branded
}

// ❌ Class-based API
export class Address {
  // Voltaire uses namespace pattern, not classes
}