Optional — Nullable Paths
Reading deep nested structures that might be missing is a problem standard TypeScript solves
elegantly. The optional chaining operator (?.) allows us to navigate through potentially absent
objects without throwing runtime errors.
However, a significant gap appears when we need to update these structures. While optional chaining
allows safe reads, there is no corresponding operator for writes. There is no ?.= operator in
JavaScript. When we want to update a value at the end of a nullable path, we are forced to write
verbose, conditional branching code filled with intermediate checks and nested object spreads.
Optional closes this gap. It represents a two-way path through a structure where elements along
the way may or may not exist. Reads through an optional return a Maybe context, while writes and
modifications automatically apply if the path is complete, and act as a safe, silent no-op if any
part of the path 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.prop, 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.make:
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:
When to use Optional vs Lens
Section titled “When to use Optional vs Lens”Use the following reference to select the correct tool for your data path:
| Situation | Tool |
|---|---|
| The target property is required and always present | Lens.prop |
The target property is declared as optional (key?: T) | Optional.prop |
The target is a specific element within an array (arr[i]) | Optional.index |
| The path starts with required fields and ends with an optional field | Lens.andThenOptional or Lens.toOptional |