lens.ts
TypeScript Lens implementation with property proxy
Lens?
Lens is composable abstraction of getter and setter. For more detail of Lens, I
recommend reading the following documents.
Install
Via npm:
npm i lens.ts
Usage
import { lens } from 'lens.ts';
type Person = {
name: string;
age: number;
accounts: Array<Account>;
};
type Account = {
type: string;
handle: string;
};
const azusa: Person = {
name: 'Nakano Azusa',
age: 15,
accounts: [
{
type: 'twitter',
handle: '@azusa'
},
{
type: 'facebook',
handle: 'nakano.azusa'
}
]
};
const personL = lens<Person>();
personL.k('name')
personL.k('accounts')
personL.k('hoge')
personL.k('accounts').k(1)
personL.k(1)
personL.name
personL.accounts
personL.accounts[1]
personL.hoge
personL.accounts[0].handle.get()(azusa)
personL.accounts[0].handle.set('@nakano')(azusa)
personL.age.set(x => x + 1)(azusa)
const fstAccountL = lens<Person>().accounts[0]
const handleL = lens<Account>().handle
fstAccountL.compose(handleL)
fstAccountL.get(handleL.get())(azusa)
fstAccountL.set(handleL.set('@nakano'))(azusa)
You can find similar example code in /test.
API
lens.ts
exports the followings:
import {
lens,
Getter,
Setter,
Lens,
createLens
} from 'lens.ts';
lens
A function lens
is a factory function for an identity lens for a type. It
returns a Lens
instance.
lens<Person>()
Getter
, Setter
They are just a type alias for the following function types.
export type Getter<T, V> = (target: T) => V;
export type Setter<T> = (target: T) => T;
Basically, Getter
is a function to retrieve a value from a target. Setter
is
a function to set or update a value in a provided target and return a new object
with a same type as the target.
Any Setter
returned from Lens
has immutable API, which means it doesn't
modify the target object.
Lens<T, U>
An instance of Lens
can be constructed with a getter and setter for a
source type T
and a result type U
.
Lens
is not just a class, but it's internally a product type of LensImpl
and
Proxy types, so you cannot just create one with new Lens()
. A recommended way
to create an instance is lens<X>()
, but you can also manually provide a getter
and a setter with createLens()
.
function createLens<T, U>(
_get: Getter<T, U>,
_set: (value: U) => Setter<T>
): Lens<T, U> {
return proxify(new LensImpl(_get, _set));
}
Lens
provides the following methods.
.k<K extends keyof U>(key: K)
Narrow the lens for a property of U
.
type Person = {
name: string;
age: number;
accounts: Account[];
};
lens<Person>.k('name')
lens<Person>.k('accounts')
lens<Person>.k('accounts').k(0)
.get()
It is polymorphic.
.get(): Getter<T, U>
.get<V>(f: Getter<U, V>): Getter<T, V>
.get()
returns a getter, which can be applied to an actual target object to
retrive an actual value. You can optionally provide another getter (or mapping
function) to retrieve a mapped value.
const target = { age: 15, ... };
const ageL = lens<Person>().k('age');
ageL.get()(target)
ageL.get(age => age + 10)(target)
.set()
It is polymorphic.
.set(val: U): Setter<T>
.set(f: Setter<U>): Setter<T>
.set()
returns a setter, which can set or update an internal value and returns
an updated (and new) object. Setter
s here should be all immutable. You can
provide a value to set, or optionally a setter for value.
const target = { age: 15, ... };
const ageL = lens<Person>().k('age');
ageL.set(20)(target)
ageL.set(age => age + 1)(target)
.compose(another: Lens<U, V>): Lens<U, V>
Compose 2 lenses into one.
let lens1: Lens<T, U>;
let lens2: Lens<U, V>;
let accountsL = lens<Person>().k('accounts');
let firstL = <T>() => lens<T[]>().k(0);
let firstAccountL =
accountsL.compose(firstL());
FYI: The reason firstL
becomes a function with <T>
is to make it
polymorphic.
Proxied properties
Lens<T, U>
also provides proxied properties for the type U
.
objL.name
arrL[0]
Credits
Property proxy couldn't have been implemented without
@ktsn's help.
License
MIT