Optional — Nullable Paths
While optional chaining (?.) allows safe reads through nullable object hierarchies, JavaScript has
no syntax for immutable updates along nullable paths. Updating nested optional properties immutably
requires multi-layer conditional checks and object spreads.
Optional<S, A> models a bidirectional path to an optional property A inside S. Reads return
Maybe<A>, while writes and modifications apply immutably if the target exists, acting as a no-op
if any segment is missing.
The problem with updating optional paths
Section titled “The problem with updating optional paths”Consider a system that tracks a user’s notification preferences, where both the preferences sub-object and individual notification channels are completely optional:
If we want to enable the Slack channel, we cannot simply write:
To update this safely and immutably using standard JavaScript, we must manually guard each nullable level to determine whether we need to create it or skip the update:
This code is incredibly difficult to read, write, and maintain. It forces the developer to manually manage the control flow of absence, mixing business logic with defensive null-checking.
The shift to nullable paths
Section titled “The shift to nullable paths”An Optional<S, A> models a traversal that might fail to reach its destination.
- Reading through an optional yields
Maybe<A>(returningSomeif the path is valid and holds a value, orNoneif any segment is absent). - Overwriting or modifying through an optional returns a new structure with the update applied if the path exists, or the original structure unchanged if the path is broken.
flowchart TD
S["Outer Structure (S)"] -- "get" --> M["Maybe<A>"]
S -- "modify(fn)" --> S2["New Structure (S)"]
S2 -- "If path exists" --> S3["Updated Structure"]
S2 -- "If path broken" --> S["Original Structure (Unchanged)"]
Creating optional paths
Section titled “Creating optional paths”We can target optional object properties using Optional.from.property, and array elements by index
using Optional.index:
If we need a custom optional path — such as parsing a string value that might be empty or invalid —
we can define it manually with Optional.from.accessors:
Reading values safely
Section titled “Reading values safely”When reading a value through an Optional, we receive a Maybe context. We then use standard
functional helpers to extract or fold the value:
Modifying and writing through optionals
Section titled “Modifying and writing through optionals”Writing or modifying a value through an Optional always returns a new object reference if the path
is resolved and a change occurs, preserving reference equality and returning the original object if
the path is broken:
Composing deep optional paths
Section titled “Composing deep optional paths”Just like lenses, optionals compose. We can combine multiple optional paths using
Optional.andThen. If any step in the composition fails, the entire chain resolves to a safe no-op
or a None value:
Bridging Lenses and Optionals
Section titled “Bridging Lenses and Optionals”It is very common for a path to start with fields that are guaranteed to exist, and then reach a
field that is optional. We can transition from a Lens to an Optional using Lens.toOptional, or
compose a lens directly using Optional.andThenLens or Lens.andThenOptional:
Problems it solves
Section titled “Problems it solves”- Updating optional nested properties safely: In user preference trees and optional
configuration blocks (such as
user?.preferences?.notifications?.emailDigest), modifying an optional leaf requires multiple existence checks to avoid modifying non-existent objects.Optionalsafely retrieves values asMaybeand immutably modifies target fields only when the path is present. - Conditional modifications without creating phantom fields (
Optional.modify): Applying updates (such as trimming an optional nickname or incrementing an optional retry count) should only take place if the field already exists.Optional.modifyapplies functions to existing targets without creating unwanted default properties. - Modifying specific items in nested array state: Updating an element at an index inside an
array nested inside state (such as
cart.items[index].quantity) requires cloning the array, checking bounds, and spreading parent objects.Optional.indexallows direct, boundary-safe updates to indexed collection elements. - Composing guaranteed and optional paths: Real-world domain models mix required structural
wrappers with optional fields.
Optionalconnects guaranteed paths (Lens) with optional leaves (Optional), providing a unified, type-safe pipeline for deep reads and writes.