accessor-ts
TypeScript icon, indicating that this package has built-in type declarations

1.5.0 • Public • Published

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.

Package Sidebar

Install

npm i accessor-ts

Weekly Downloads

2

Version

1.5.0

License

MIT

Unpacked Size

57.6 kB

Total Files

27

Last publish

Collaborators

  • jethrolarson