Partial optics
By default an optic will always be able to retrieve the value it's focused on, we say that's it's total.
But an optic can also be partial, meaning it focuses on a value that might not exist.
When you read a value focused by a partial optic you might get undefined
if the value is not present.
When you derive new optics from an optional or nullable property, the resulting optics end up partial
.
As an example let's create users with an optional contact
property:
ts
constinitialUsers : {name : string;contact ?: {phone : string } }[] = [{name : "Vincent",contact : {phone : "999-999-999" } },{name : "Gabin" },];constuserOptics =createState (initialUsers );
ts
constinitialUsers : {name : string;contact ?: {phone : string } }[] = [{name : "Vincent",contact : {phone : "999-999-999" } },{name : "Gabin" },];constuserOptics =createState (initialUsers );
Here some users might not have any contact information, so the contact
field is optional.
Every optic we derive from contact
ends up partial:
ts
constfirstUserPhoneOptic =userOptics [0].contact .phone ;constsecondUserPhoneOptic =userOptics [1].contact .phone ;
ts
constfirstUserPhoneOptic =userOptics [0].contact .phone ;constsecondUserPhoneOptic =userOptics [1].contact .phone ;
A partial optic returns undefined
if one of the value in the path is not present (in our case contact
), that's why the return type of get
is T | undefined
:
ts
constvincentsPhone =firstUserPhoneOptic .get ();constgabinsPhone =secondUserPhoneOptic .get ();// vincentsPhone = "999-999-999"// gabinsPhone = undefined
ts
constvincentsPhone =firstUserPhoneOptic .get ();constgabinsPhone =secondUserPhoneOptic .get ();// vincentsPhone = "999-999-999"// gabinsPhone = undefined
When trying to update a value with a partial that is not focused on a value, the update is ignored:
ts
secondUserPhoneOptic .set ("888-888-888");secondUserPhoneOptic .get (); // undefined
ts
secondUserPhoneOptic .set ("888-888-888");secondUserPhoneOptic .get (); // undefined
On a plain JS object if you have to use the optional chaining operator ?.
on the path to a property,
then it means using the same path on the optic will get you a partial optic (no need to use ?.
on optics though).
ts
initialUsers [0].contact ?.phone ;userOptics [0].contact .phone ;
ts
initialUsers [0].contact ?.phone ;userOptics [0].contact .phone ;
Another way you can end up with partial optics is with some combinators.
For exemple with an optic focused on an array, the find
combinator returns a partial optic because no element of the array might satisfy the predicate.
ts
import {find } from "@optics/react/combinators";constnumbersOptic =createState ([1, 2, 3, 4]);constgreaterThanThreeOptic =numbersOptic .derive (find ((n ) =>n > 3));greaterThanThreeOptic .get (); // 4numbersOptic [3].set (0);greaterThanThreeOptic .get (); // undefined
ts
import {find } from "@optics/react/combinators";constnumbersOptic =createState ([1, 2, 3, 4]);constgreaterThanThreeOptic =numbersOptic .derive (find ((n ) =>n > 3));greaterThanThreeOptic .get (); // 4numbersOptic [3].set (0);greaterThanThreeOptic .get (); // undefined
Narrowing
We can assign a total optic to a partial one (widening the type):
ts
constnumberOptic =createState (42);constnumberPartialOptic :Optic <number,partial > =numberOptic ; // ✅ allowed
ts
constnumberOptic =createState (42);constnumberPartialOptic :Optic <number,partial > =numberOptic ; // ✅ allowed
However the reverse is not true, you can't narrow from partial to a total as it isn't type-safe:
ts
constevenNumberOptic =createState ([1, 2, 3]).derive (find ((n ) =>n % 2 === 0));constType 'Optic<number, partial>' is not assignable to type 'Optic<number>'. The types returned by 'get()' are incompatible between these types. Type 'number | undefined' is not assignable to type 'number'. Type 'undefined' is not assignable to type 'number'.2322Type 'Optic<number, partial>' is not assignable to type 'Optic<number>'. The types returned by 'get()' are incompatible between these types. Type 'number | undefined' is not assignable to type 'number'. Type 'undefined' is not assignable to type 'number'.: numberTotalOptic Optic <number> =evenNumberOptic ;
ts
constevenNumberOptic =createState ([1, 2, 3]).derive (find ((n ) =>n % 2 === 0));constType 'Optic<number, partial>' is not assignable to type 'Optic<number>'. The types returned by 'get()' are incompatible between these types. Type 'number | undefined' is not assignable to type 'number'. Type 'undefined' is not assignable to type 'number'.2322Type 'Optic<number, partial>' is not assignable to type 'Optic<number>'. The types returned by 'get()' are incompatible between these types. Type 'number | undefined' is not assignable to type 'number'. Type 'undefined' is not assignable to type 'number'.: numberTotalOptic Optic <number> =evenNumberOptic ;
You can cast a partial
optic to a total one with the whenFocused
function returned by useOptic
:
tsx
const [, {whenFocused }] =useOptic (evenNumberOptic );whenFocused ((evenNumberTotalOptic ) => {});
tsx
const [, {whenFocused }] =useOptic (evenNumberOptic );whenFocused ((evenNumberTotalOptic ) => {});
The callback will only run when the optic is focused on a value, so that it's safe to cast the optic to total inside it.
In most cases your components should only expect total optics in their props.
Use whenFocused
to narrow the eventual partial
optics before passing them to components.