Simplified Lenses implementation, typed. Object property accessor. FP & OOP friendly.


Keywords
prop, property, lens, accessor, wrapper, map, over, object, value, modify, immutable, ts, typescript, functional, fp
License
MIT
Install
npm install ts-propper@3.0.0

Documentation

build codecov Code Style: Google

ts-propper

Simplified Lenses for TypeScript. For those who find the traditional (otherwise great) Functional Lenses implementation too overwhelming to start with.

  • Easy to use.
  • Typed. With d.ts for Javascript.
  • Functional programming friendly.
  • Immutable.
  • Well tested.
  • 100% code coverage.

In a short, propper is basically a property accessor. Can read and manipulate the property it points (focuses) to.
With immutability in mind.

With propper, you can:

  • View object's property value.
  • Set object's property value.
  • Evaluate object's property value by calling a function over it.
  • Set object's property value by calling a function over the property.

Think of propper as a "better getter/setter" layer that helps other code to not use the object's internal structure.

Why use propper

  • Immutable. Instead modifying object's property, propper create a deep copy of that object, with new property value.
  • Prevents property access logic duplication, whenever a property is used. If an object structure design is changed, the only things to be modified in your code are proppers for that object.

Installation

$ npm install ts-propper

Usage

Javascript / CommonJS:

const createPropper = require('ts-propper').default;

Typescript / ES module:

import createPropper from 'ts-propper';

Example

NOTE: for a javascript example, see js-example

Let's create some type and its instances first:

// Circle type:
type Circle = {
  r: number; //radius
  center: [x: number, y: number];
  common: {
    color: string;
    id: string;
  };
};

// Create some instances:
const circ1: Circle = {
  r: 5,
  center: [1, 2],
  common: {color: '#00ff00', id: 'circle-1'},
};

const circ2: Circle = {
  r: 4,
  center: [1, 1],
  common: {color: '#ff0000', id: 'circle-2'},
};
//

Let's import the Propper package now:

import createPropper from 'ts-propper';

For every property of Circle type, we can create a propper. That propper then serves for an arbitrary number of Circle instances.

// Radius propper of a Circle type and its subtypes:
//   Radius property has a name "r" and is of a type "number"
const radiusPropper = createPropper<Circle, number>('r');

// get radius
const r1 = radiusPropper.view(circ1);
console.log('r1:', r1);
//=> r1: 5
const r2 = radiusPropper.view(circ2);
console.log('r2:', r2);
//=> r2: 4

We can also create a propper for an arbitrarily nested property of the object, using a dot notation:

const colorPropper = createPropper<Circle, string>('common.color');

// get the color
const c = colorPropper.view(circ1);
console.log('color:', c);
//=> color: #00ff00

// It also works for array item property:
//   Center point x-coord propper
const centerXPropper = createPropper<Circle, number>('center.0');
console.log('cent x:', centerXPropper.view(circ1));
//=> cent x: 1

// Note: traditional Lenses use a functional composition to access a nested property.

We can specify (possibly nested) path using an array of keys.
Also, using array in a propper creation, we can address a property inaccessible by a dot notation.

const colorPropper2 = createPropper<Circle, string>(['common', 'color']);

// get the color
const c2 = colorPropper2.view(circ1);
console.log('color:', c2);
//=> color: #00ff00

We cannot create a propper without telling its property name:

const noProp = createPropper<Circle, number>('');
//raises Error

const noProp2 = createPropper<Circle, number>([]);
//raises Error

Propper's methods do not modify the object, they return its deep copy.

The set method returns a deep copy of an object, with its property set to a new value:

const greenCircle = colorProp.set('green')(circ1);

console.log('new obj color:', colorProp.view(greenCircle));
//=> new obj color: "green"
console.log('old obj color:', colorProp.view(circ1));
//=> old obj color: "#00ff00"

The syntax of Propper's methods is functional friendly.

const darkCircles = [circ1, circ2].map(colorPropper.set('black'));
console.log('dark circles:', darkCircles);

The over method applies a function to the property:

const twoTimesBiggerCircle = radiusPropper.over(x => 2 * x)(circ1);

The evaluate method just computes a result from the property value:

const isValueBig = (x: number): boolean => x >= 10;

console.log('big radius:', radiusPropper.evaluate(isValueBig)(circ1));
//=> big radius: false

Property presence check

Before accessing the object's property, Propper checks object's property for the presence. Continues if "property value !== undefined", or throws an error.

This strict property presence checking behavior is not as powerful as allowing Propper to create new property of some object, on the fly. This behavior is a design decision, for two reasons:

  1. Removes sort of spelling errors: no magically-created unwanted new properties.
  2. It is easier to implement (and understand) in a type safe way in TypeScript.

On Strictness

You can define a Propper of unknown property of an Object:

const unknownPropper = createPropper<Circle, string>('notThere');

The safeView method of this Propper instance just returns undefined:

console.log('unknownPropper value:', unknownPropper.safeView(circ1));
//=> unknownPropper value: undefined

However, Propper's other methods raise an Error:

unknownPropper.set('something')(circ1);
// Error: Property with key path [notThere] not found at the object.

A less restrictive Propper

This Propper will work on all Objects having an 'r' property of type 'number', at the top-level of that object:

const justRPropper = createPropper<{r: number}, number>('r');

console.log(justRPropper.set(100)(circ1).r);
//=> 100

console.log(justRPropper.view({r: 2}));   // You see? Works with the {r: 2} object
//=> 2

Other Resources