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
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,
},
},
});
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));
}
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