Skip to main content

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
const userOptic = createState({ name: "Vincent" });
const userOptic: Optic<{ name: string; }, total>
const name = userOptic.name.get();
const name: string
ts
const userOptic = createState({ name: "Vincent" });
const userOptic: Optic<{ name: string; }, total>
const name = userOptic.name.get();
const name: string
total is the default

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
const initialUsers: { name: string; contact?: { phone: string } }[] = [
{ name: "Vincent", contact: { phone: "999-999-999" } },
{ name: "Gabin" },
];
 
const userOptics = createState(initialUsers);
ts
const initialUsers: { name: string; contact?: { phone: string } }[] = [
{ name: "Vincent", contact: { phone: "999-999-999" } },
{ name: "Gabin" },
];
 
const userOptics = 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
const firstUserPhoneOptic = userOptics[0].contact.phone;
const firstUserPhoneOptic: Optic<string, partial>
 
const secondUserPhoneOptic = userOptics[1].contact.phone;
const secondUserPhoneOptic: Optic<string, partial>
ts
const firstUserPhoneOptic = userOptics[0].contact.phone;
const firstUserPhoneOptic: Optic<string, partial>
 
const secondUserPhoneOptic = userOptics[1].contact.phone;
const secondUserPhoneOptic: Optic<string, partial>

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
const vincentsPhone = firstUserPhoneOptic.get();
const vincentsPhone: string | undefined
 
const gabinsPhone = secondUserPhoneOptic.get();
const gabinsPhone: string | undefined
 
// vincentsPhone = "999-999-999"
// gabinsPhone = undefined
ts
const vincentsPhone = firstUserPhoneOptic.get();
const vincentsPhone: string | undefined
 
const gabinsPhone = secondUserPhoneOptic.get();
const gabinsPhone: string | undefined
 
// 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
tip

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;
(property) phone: string | undefined
 
userOptics[0].contact.phone;
(property) phone: Optic<string, partial>
ts
initialUsers[0].contact?.phone;
(property) phone: string | undefined
 
userOptics[0].contact.phone;
(property) phone: Optic<string, partial>

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";
 
const numberOptic = createState(42);
 
const evenNumberOptic = numberOptic.derive(cond((n) => n % 2 === 0));
const evenNumberOptic: Optic<number, partial>
 
evenNumberOptic.get(); // 42
 
evenNumberOptic.set((n) => n + 1);
 
numberOptic.get(); // 43
evenNumberOptic.get(); // undefined
ts
import { cond } from "@optics/react/combinators";
 
const numberOptic = createState(42);
 
const evenNumberOptic = numberOptic.derive(cond((n) => n % 2 === 0));
const evenNumberOptic: Optic<number, partial>
 
evenNumberOptic.get(); // 42
 
evenNumberOptic.set((n) => n + 1);
 
numberOptic.get(); // 43
evenNumberOptic.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
const numberOptic = createState(42);
 
const numberPartialOptic: Optic<number, partial> = numberOptic; // ✅ allowed
ts
const numberOptic = createState(42);
 
const numberPartialOptic: 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
const evenNumberOptic = createState(42).derive(cond((n) => n % 2 === 0));
const evenNumberOptic: Optic<number, partial>
 
const numberTotalOptic: Optic<number, total> = evenNumberOptic;
Type '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'.
ts
const evenNumberOptic = createState(42).derive(cond((n) => n % 2 === 0));
const evenNumberOptic: Optic<number, partial>
 
const numberTotalOptic: Optic<number, total> = evenNumberOptic;
Type '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'.