Skip to main content

Overview

The lazy() decoder allows you to create self-referential decoders for recursive data structures. It defers the evaluation of a decoder by wrapping it in a function, which is essential for handling types like trees, linked lists, or nested comments.

Basic Usage

import { lazy, object, string, array, optional } from 'decoders';

type Category = {
  name: string;
  subcategories: Category[];
};

const categoryDecoder: Decoder<Category> = object({
  name: string,
  // Use lazy to reference the decoder itself
  subcategories: array(lazy(() => categoryDecoder)),
});

const category = categoryDecoder.verify({
  name: 'Electronics',
  subcategories: [
    {
      name: 'Computers',
      subcategories: [
        { name: 'Laptops', subcategories: [] },
        { name: 'Desktops', subcategories: [] },
      ],
    },
    { name: 'Phones', subcategories: [] },
  ],
});
// Type: Category (with nested subcategories)

Type Signature

function lazy<T>(decoderFn: () => Decoder<T>): Decoder<T>;

Parameters

decoderFn
() => Decoder<T>
required
A function that returns the decoder. The function is called each time the decoder needs to be evaluated, allowing for circular references.

Return Value

Returns a Decoder<T> that evaluates the provided decoder function lazily.

Examples

Tree structure

import { lazy, object, string, number, array } from 'decoders';

type TreeNode = {
  value: number;
  label: string;
  children: TreeNode[];
};

const treeNodeDecoder: Decoder<TreeNode> = object({
  value: number,
  label: string,
  children: array(lazy(() => treeNodeDecoder)),
});

const tree = treeNodeDecoder.verify({
  value: 1,
  label: 'root',
  children: [
    {
      value: 2,
      label: 'child1',
      children: [
        { value: 3, label: 'grandchild', children: [] },
      ],
    },
    { value: 4, label: 'child2', children: [] },
  ],
});

Linked list

import { lazy, object, string, nullable } from 'decoders';

type ListNode = {
  value: string;
  next: ListNode | null;
};

const listNodeDecoder: Decoder<ListNode> = object({
  value: string,
  next: nullable(lazy(() => listNodeDecoder)),
});

const list = listNodeDecoder.verify({
  value: 'first',
  next: {
    value: 'second',
    next: {
      value: 'third',
      next: null,
    },
  },
});

Comment threads

import { lazy, object, string, number, array } from 'decoders';

type Comment = {
  id: number;
  author: string;
  text: string;
  replies: Comment[];
};

const commentDecoder: Decoder<Comment> = object({
  id: number,
  author: string,
  text: string,
  replies: array(lazy(() => commentDecoder)),
});

const thread = commentDecoder.verify({
  id: 1,
  author: 'Alice',
  text: 'Great article!',
  replies: [
    {
      id: 2,
      author: 'Bob',
      text: 'Thanks!',
      replies: [
        {
          id: 3,
          author: 'Alice',
          text: 'You\'re welcome!',
          replies: [],
        },
      ],
    },
  ],
});

File system structure

import { lazy, object, string, either, array } from 'decoders';

type File = {
  type: 'file';
  name: string;
  size: number;
};

type Directory = {
  type: 'directory';
  name: string;
  contents: FileSystemNode[];
};

type FileSystemNode = File | Directory;

const fileDecoder: Decoder<File> = object({
  type: constant('file'),
  name: string,
  size: number,
});

const directoryDecoder: Decoder<Directory> = object({
  type: constant('directory'),
  name: string,
  contents: array(lazy(() => fileSystemNodeDecoder)),
});

const fileSystemNodeDecoder: Decoder<FileSystemNode> = either(
  fileDecoder,
  directoryDecoder
);

const fs = fileSystemNodeDecoder.verify({
  type: 'directory',
  name: 'root',
  contents: [
    { type: 'file', name: 'readme.txt', size: 1024 },
    {
      type: 'directory',
      name: 'src',
      contents: [
        { type: 'file', name: 'index.ts', size: 2048 },
      ],
    },
  ],
});

JSON-like structure

import { lazy, either, string, number, boolean, null_, array, record } from 'decoders';

type JsonValue =
  | string
  | number
  | boolean
  | null
  | JsonValue[]
  | { [key: string]: JsonValue };

const jsonValueDecoder: Decoder<JsonValue> = either(
  string,
  number,
  boolean,
  null_,
  array(lazy(() => jsonValueDecoder)),
  record(lazy(() => jsonValueDecoder))
);

const json = jsonValueDecoder.verify({
  name: 'Project',
  version: 1,
  active: true,
  tags: ['typescript', 'validation'],
  metadata: {
    created: '2024-01-01',
    stats: {
      downloads: 1000,
      stars: 50,
    },
  },
});

Why Lazy Is Needed

Without lazy(), attempting to create recursive decoders would result in initialization errors:
// This doesn't work!
const categoryDecoder = object({
  name: string,
  subcategories: array(categoryDecoder), // Error: used before initialization
});
The lazy() function defers the decoder reference:
// This works!
const categoryDecoder: Decoder<Category> = object({
  name: string,
  subcategories: array(lazy(() => categoryDecoder)), // OK: wrapped in function
});

Implementation Details

The lazy() decoder simply wraps the decoder function and calls it when needed:
// Simplified implementation
export function lazy<T>(decoderFn: () => Decoder<T>): Decoder<T> {
  return define((blob) => decoderFn().decode(blob));
}

Performance Considerations

Each time lazy() is invoked during decoding, it calls the provided function to get the decoder. For deeply nested structures, this can add overhead. However, this is typically negligible compared to the actual validation work.
The decoder function is called every time the lazy decoder is used, so avoid expensive operations inside the function. Simply return a reference to an existing decoder.

Error Handling

Error messages from lazy decoders work the same as regular decoders:
import { lazy, object, string, array } from 'decoders';

type Category = {
  name: string;
  subcategories: Category[];
};

const categoryDecoder: Decoder<Category> = object({
  name: string,
  subcategories: array(lazy(() => categoryDecoder)),
});

try {
  categoryDecoder.verify({
    name: 'Electronics',
    subcategories: [
      { name: 'Invalid', subcategories: 'not-an-array' },
    ],
  });
} catch (error) {
  console.error(error.message);
  // Error points to the exact location in the nested structure
}

Type Annotations

When using lazy(), you typically need to explicitly annotate the decoder’s type:
// Need explicit type annotation
const categoryDecoder: Decoder<Category> = object({
  name: string,
  subcategories: array(lazy(() => categoryDecoder)),
});

// Without annotation, TypeScript can't infer the recursive type
  • array - Often used with lazy for recursive arrays
  • either - Combine with lazy for polymorphic recursive types
  • optional - For optional recursive references
  • nullable - For nullable recursive references

See Also

Build docs developers (and LLMs) love