accessor-ts
v1.5.0
Published
A TS/JS library for doing immutable updates and querying on nested data structures in a way that is composable and powerful.
Downloads
94
Maintainers
Readme
accessor-ts
Accessor-ts is a library for doing immutable updates and querying on nested data structures in a way that is composable and powerful. This is similar to lens and traversal libraries like partial.lenses, monacle-ts, and shades. This library aims to allow easy typed composition of optics without the bewildering functional programming jargon that usually comes with them.
Installation
npm install -S accessor-ts
Prerequisites
- This library and its examples uses currying, arrow functions, and generics extensively so it'll help if you're familiar with those concepts.
- TypeScript - While usage without TypeScript will work, you lose a lot of the benefit of this library without types. If you're looking for an optics library for use without TS check out partial.lenses.
Examples
import { prop, index, filter, set, all, comp } from "accessor-ts";
Simple property query
prop<{ a: number }>()("a").query({ a: 1 });
While you can inline an interface for your Accessors you probably want to define your interfaces separately and then create your accessors to match.
// Sample interface
interface User {
name: string;
id: number;
cool?: boolean;
connections: number[];
}
// Sample User
const bob: User = { name: "bob", id: 1, connections: [1, 2] };
Partially applied accessors can be stored, bound to the interface's type
const userProps = prop<User>();
set
an accessor to immutably modify its target
set(userProps("id"))(3)(bob); // => { name: "bob", id: 3, connections: [1, 2] }
Trying to pass an invalid key to our accessor will be caught by TypeScript
userProps("invalid"); // `Argument of type '"invalid"' is not assignable to parameter of type '"id" | "name" | "cool" | "connections"'.ts(2345)`
You can query for optional fields
userProps("cool").query(bob); // => [undefined]
Accessors are composable so that you can extract or modify nested data structures.
interface Friend {
user: User;
}
const friendProps = prop<Friend>();
const myFriendBob: Friend = { user: bob };
comp(friendProps("user"), userProps("id")).query(myFriendBob); // => [1]
This is the same as myFriendBob.user.id
.
The composed accessors are accessors themselves and can be stored and reused
const friendName = comp(friendProps("user"), userProps("name"));
set(friendName)("Robert")(myFriendBob); // => {user: {name: "Robert", id: 1, connections: [1, 2]}}
We can use Accessor.mod to run a function on the targeted value
comp(userProp("id")).mod(a => a + 1)(bob); // => { name: "bob", id: 2, connections: [1, 2] }
index
can be used to focus a specific element of an array
comp(friendProps("user"), userProps("connections"), index(1)).query(
myFriendBob
); // => [1]
all()
can be used to target all items within a nested array
comp(friendProps("user"), userProps("connections"), all()).query(myFriendBob); // => [1, 2]
all
gets much more interesting when we have Arrays within Arrays
interface Friends {
friends: Friend[];
}
const shari: User = { name: "Shari", id: 0, connections: [3, 4] };
const myFriendShari: Friend = { user: shari };
const baz: Friends = { friends: [myFriendBob, myFriendShari] };
const makeAllFriendsCool = set(
comp(
prop<Friends>()("friends"),
all(),
friendProps("user"),
userProps("cool")
)
)(true);
makeAllFriendsCool(baz); // => Sets "cool" to true for all the users within
filter
can be used to reduce the scope of an accessor to items which pass a test function. This doesn't remove items from the data structure but just changes what you get from queries or modify.
const isOdd = (a: number): boolean => a % 2 === 1;
// accessor chain as reusable value
const oddConnectionsOfFriends = comp(
prop<Friends>()("friends"),
all(),
friendProps("user"),
userProps("connections"),
filter(isOdd)
);
oddConnectionsOfFriends.query(baz) // => [1, 3]
set(oddConnectionsOfFriends)(NaN)(baz)); /* =>
{friends: [
{user: {name: "bob", id: 1, connections: [NaN, 2]}},
{user: {name: "Shari", id: 0, connections: [NaN, 4]}}
]} */
API
Accessors
Accessors are the core of this library and have the interface:
// S is the type of the data structure that will be operated on, A is the type of some value(s) within
export interface Accessor<S, A> {
// get an array of result(s) from the data structure
query(struct: S): A[];
// modify item(s) within the data structure using the passed function
mod(fn: (x: A) => A): (struct: S) => S;
}
Since accessor-ts only provides Accessors for arrays and objects you may want to create your own if you use other data structures like Set
, Map
or immutable.js
prop
: <Obj>() => <K extends keyof Obj>(k: K) => Accessor<Obj, Obj[K]>;
Create Accessor that points to a property of an object
Example:
prop<Person>()('name').query(bob) // => ['bob']
index
: <A>(i: number) => Accessor<A[], A>;
Create Accessor that points to an index of an array
Example:
index(1).query([1, 2, 3]) // => [2]
set
: <S, A>(acc: Accessor<S, A>) => (x: A) => (s: S) => S
Immutably assign using an Accessor
Example:
set(prop<Person>()('name'))('Robert')(bob) // => {name: 'Robert', ...}
comp
: <A, B, C>(acc1: Accessor<A, B>, acc2: Accessor<B, C>) => Accessor<A, C>
Compose 2 or more Accessors (overloaded up to 8)
Examples:
comp(prop<Person>()('address'), prop<Address>()('city')).query(bob) // => ['Seattle']
all
: <A>() => Accessor<A[], A>
Create Accessor focused on all items in an array. query
unwraps them, mod
changes each item.
Examples:
const makeAllFriendsCool = (user: Person) => set(comp(prop<Person>()('friends'), all<Person>(), prop<Person>()('isCool'))(true).query(user)
// BTW you can make functions point-free if you like:
const getFriends = comp(prop<Person>()('friends'), all<Person>()).query
// is the same as
const getFriends = (user: Person) => comp(prop<Person>()('friends'), all<Person>()).query(user)
filter
: <A>(pred: (x: A) => boolean) => Accessor<A[], A>
Create Accessor that targets items in an array that match the passed predicate. query
returns the matched items, mod
modifies matched items.
Example:
const getCoolFriends = (user: Person) => comp(prop<Person>()('friends'), filter<Person>(friend => friend.isCool)).query(user);
before
: <A>(i: number) => Accessor<A[], A>
Create Accessor that targets items in an array before the passed index
Example:
const getFirstTenFriends = comp(prop<Person>()('friends'), before(10)).query
after
: <A>(i: number) => Accessor<A[], A>
Create Accessor that targets items in an array after the passed index
Example:
const getMoreFriends = comp(prop<Person>()('friends'), after(9)).query
sub
<SSub, S extends SSub = never>(keys: Array<keyof SSub>) => Accessor<S, SSub>
Create an accessor that targets a subset of properties of an object.
Example:
interface Entity {
name: string;
id: number;
}
const entityAcc = sub<Entity, User>(['name', 'id'])
entityAcc.query(bob) // => [{name: 'bob', id: 1}]
unit
: <A>(): Accessor<A, A>
No-op Accessor
Makes Accessors a monoid in conjunction with comp
. You'll probably only need this if you're writing really abstract code.
Example:
comp(prop<Person>()('name'), unit<String>()).query(bob) // => ['bob']
Utilities
flow
<A, B, C>(f: (x: A) => B, g: (y: B) => C) => (x: A) => C
Compose two functions left to right.
K
<A>(a: A) => (_b: unknown) => A
Constant combinator. Returns a function that ignores its argument and returns the original one.
empty
<A>() => A[]
Returns an empty array.
flatmap
<T, U>(f: (x: T) => U[]) => (xs: T[]) => U[]
Apply an array returning function to each item in an array and return an unnested array.
removeAt
(index: number) => <T>(xs: T[]) => T[]
Removes item at passed index from array.
prepend
<A>(x: A) => (xs: A[]): A[]
Prepend an item to an array.
append
<A>(x: A) => (xs: A[]): A[]
Append an item to the end of an array
head
<A>(xs: A[]) => A | undefined
Return the first item in an array
tail
<A>(xs: A[]) => A[]
Return a copy of the array excluding the first item.
not
(a: boolean) => boolean
Logically negate the argument.
mergeInto
<State>(part: Partial<State>) => (s: State) => State
Merge a partial object into the full one. Useful for updating a subset of properties of an object.
Weaknesses
- Since
query
returns an array of results, users must be careful about the array being empty. - Performance of this library hasn't been evaluated or optimized yet.
Comparisons to other libries
- shades: Shades' usage is more terse, and doesn't require binding the types of its optics to interfaces. Shades' types are harder to understand and leverage, especially since its source isn't in TypeScript. Accessor-ts only has one type so users don't have to understand the differences between Lenses, Isomorphisms, and Traversals. Shades has a massive generated type file that is impossible to grok and slows down the TS compiler on medium to large projects (in my experience).
- monacle-ts: Accessor-ts' usage is simpler as there is only one composition operator. monacle-ts has way more concepts to learn and use. monacle-ts has dependencies on fp-ts which is difficult to learn and leverage. monacle-ts is more expressive, mature, and powerful.