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:
tsconstinitialUsers : {name : string;contact ?: {phone : string } }[] = [{name : "Vincent",contact : {phone : "999-999-999" } },{name : "Gabin" },];constuserOptics =createState (initialUsers );
tsconstinitialUsers : {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:
tsconstfirstUserPhoneOptic =userOptics [0].contact .phone ;constsecondUserPhoneOptic =userOptics [1].contact .phone ;
tsconstfirstUserPhoneOptic =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:
tsconstvincentsPhone =firstUserPhoneOptic .get ();constgabinsPhone =secondUserPhoneOptic .get ();// vincentsPhone = "999-999-999"// gabinsPhone = undefined
tsconstvincentsPhone =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:
tssecondUserPhoneOptic .set ("888-888-888");secondUserPhoneOptic .get (); // undefined
tssecondUserPhoneOptic .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).
tsinitialUsers [0].contact ?.phone ;userOptics [0].contact .phone ;
tsinitialUsers [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.
tsimport {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
tsimport {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):
tsconstnumberOptic =createState (42);constnumberPartialOptic :Optic <number,partial > =numberOptic ; // ✅ allowed
tsconstnumberOptic =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:
tsconstevenNumberOptic =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 ;
tsconstevenNumberOptic =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:
tsxconst [, {whenFocused }] =useOptic (evenNumberOptic );whenFocused ((evenNumberTotalOptic ) => {});
tsxconst [, {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.