Skip to main content
Version: Canary 🚧

Mutator API

Immutable State Updates

The mutator module provides a functional, transaction-based API for modifying Typst dictionaries.

Why use a Mutator?

While Typst variables are mutable within their scope, updating deeply nested structures often requires verbose "copy-modify-assign" patterns. The Mutator API abstracts this complexity, allowing you to describe a transaction of changes cleanly without manually reconstructing the dictionary hierarchy or writing repetitive update logic.

The Batch Transaction​

The core concept is the batch function, which applies a sequence of operations to a target dictionary and returns the new state.

batch​

Applies a list of operations to a target dictionary.

loom.mutator.batch(target, ops)
ParameterTypeDefaultDescription
targetdictionarynoneRequired
opsarrayRequiredA block or array of operation functions (created by put, update, etc.) to apply sequentially.
Syntax Sugar

You can pass a code block { ... } as the ops argument. Inside this block, simply call the operation functions. Typst automatically collects these calls into an array for the batch processor.

Example:

#import "@preview/loom:0.1.0": mutator

#let state = (count: 0, user: "Guest")

#let new-state = mutator.batch(state, {
import mutator: *
put("user", "Admin")
update("count", c => c + 1)
})

Operations​

These functions generate Operation Objects. They define what to do, but the change only happens when processed by batch.

Context Usage

These functions are not standalone. They must be used inside the ops list passed to a batch or nest call.

Path Traversal

Most operations accept an optional variable number of arguments (..path) before the key to traverse deeply into nested dictionaries without needing explicit nest calls.

put​

Sets a key to a specific value. Overwrites the value if the key already exists.

put(..path, key, value)
ParameterTypeDefaultDescription
pathstr()Optional path of keys to traverse.
keystrRequiredThe dictionary key to set.
valueanyRequiredThe value to assign to the key.

ensure​

Sets a value only if the key is missing (or none).

ensure(..path, key, default-value)
ParameterTypeDefaultDescription
pathstr()Optional path of keys to traverse.
keystrRequiredThe dictionary key to check.
default-valueanyRequiredThe value to assign if the key does not exist.

derive​

Sets a value, inheriting the previous one if the new value is auto.

  • If value is auto: uses the current state value.
  • If current state is missing (and value is auto): uses default.
  • If value is set: uses that value.
derive(..path, key, value, default: none)
ParameterTypeDefaultDescription
pathstr()Optional path of keys to traverse.
keystrRequiredThe dictionary key to update.
valueanyRequiredThe new value (or auto).
defaultanynoneFallback value if value is auto and the key is missing in the current state.

update​

Transforms an existing value using a callback function.

Existing Keys Only

The callback is only executed if the key already exists in the dictionary (and is not none). If the key is missing, this operation does nothing. Use put or ensure if you need to initialize values.

update(..path, key, callback)
ParameterTypeDefaultDescription
pathstr()Optional path of keys to traverse.
keystrRequiredThe dictionary key to update.
callbackfunctionRequiredA function (current) => new that receives the current value and returns the new one.

remove​

Deletes a key from the dictionary.

remove(..path, key)
ParameterTypeDefaultDescription
pathstr()Optional path of keys to traverse.
keystrRequiredThe dictionary key to remove.

merge​

Merges another dictionary into the current state.

merge(..path, other-dictionary)
ParameterTypeDefaultDescription
pathstr()Optional path of keys to traverse.
other-dictionarydictionaryRequiredThe dictionary to merge into the current state.
Shallow Merge

This operation performs a shallow merge. Nested dictionaries in other-dictionary will overwrite those in the state. For deep merging, use merge-deep.

merge-deep​

Recursively merges another dictionary into the current state.

merge-deep(..path, other-dictionary)
ParameterTypeDefaultDescription
pathstr()Optional path of keys to traverse.
other-dictionarydictionaryRequiredThe dictionary to merge recursively into the current state.
Use Case

Use merge-deep when you want to apply a configuration patch that contains nested settings without wiping out the existing sibling keys in those nested objects.

ensure-deep​

Ensures that a dictionary structure exists deeply. Unlike merge-deep, this treats the input defaults as fallback values.

  • If a key exists in the current state, the current value is preserved.
  • If a key is missing, the value from defaults is used.
  • Nested dictionaries are merged recursively.
ensure-deep(..path, defaults)
ParameterTypeDefaultDescription
pathstr()Optional path of keys to traverse.
defaultsdictionaryRequiredThe dictionary containing default structure and values.

Example:

let defaults = (
theme: (color: "blue", font: "serif"),
meta: (version: 1)
)

// If state was: (theme: (color: "red"))
// Result is: (theme: (color: "red", font: "serif"), meta: (version: 1))
ensure-deep(defaults)