Total and partial optics
An optic can either be total
which means it's focused on one value, or partial
: focused on zero or one value.
createState
returns a total optic, it will never fail to focus on the root state:
ts
constuserOptic =createState ({name : "Vincent" });constname =userOptic .name .get ();
ts
constuserOptic =createState ({name : "Vincent" });constname =userOptic .name .get ();
When you declare an optic without specifying the second type parameter then it defaults to total
:
Optic<string>
=== Optic<string, total>
Partial optics
A partial optic focuses on a value that might not exist.
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 (hence the question mark).
When you have an optic focused on an optional (or nullable) property, then the new optics derived from it will be partial.
In our case it means every optics 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 ;
An object being nullable doesn't mean we shouldn't be able to focus values inside it.
But we should still be made aware of the fact that getting the value could yield undefined
, and that's what partial
is for.
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 that a partial fails to reach then it simply no-op:
ts
secondUserPhoneOptic .set ("888-888-888");secondUserPhoneOptic .get (); // undefined
ts
secondUserPhoneOptic .set ("888-888-888");secondUserPhoneOptic .get (); // undefined
On a plain javascript object if you have to use the optional chaining operator ?.
on the path to a property,
then 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 ;
Deriving optics from properties of optional objects is not the only way to get partial optics.
For exemple with an optic focused on an array, the find
combinators returns a partial optic because no element of the array might match the predicate.
Or again the cond
combinator returns a partial optic because the condition might not be met by the focused value:
ts
import {cond } from "@optics/react/combinators";constnumberOptic =createState (42);constevenNumberOptic =numberOptic .derive (cond ((n ) =>n % 2 === 0));evenNumberOptic .get (); // 42evenNumberOptic .set ((n ) =>n + 1);numberOptic .get (); // 43evenNumberOptic .get (); // undefined
ts
import {cond } from "@optics/react/combinators";constnumberOptic =createState (42);constevenNumberOptic =numberOptic .derive (cond ((n ) =>n % 2 === 0));evenNumberOptic .get (); // 42evenNumberOptic .set ((n ) =>n + 1);numberOptic .get (); // 43evenNumberOptic .get (); // undefined
Type relations
total
is a subtype of partial
, meaning 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, assigning a partial optic to a total one (narrowing the type) fails to compile:
ts
constevenNumberOptic =createState (42).derive (cond ((n ) =>n % 2 === 0));constType 'Optic<number, partial>' is not assignable to type 'Optic<number, total>'. Type 'Optic<number, partial>' is not assignable to type '_Optic<number, total>'. 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, total>'. Type 'Optic<number, partial>' is not assignable to type '_Optic<number, total>'. 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,total > =evenNumberOptic ;
ts
constevenNumberOptic =createState (42).derive (cond ((n ) =>n % 2 === 0));constType 'Optic<number, partial>' is not assignable to type 'Optic<number, total>'. Type 'Optic<number, partial>' is not assignable to type '_Optic<number, total>'. 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, total>'. Type 'Optic<number, partial>' is not assignable to type '_Optic<number, total>'. 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,total > =evenNumberOptic ;