Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

monocle-ts

gcanti1.1mMIT2.3.13TypeScript support: included

A porting of scala monocle library to TypeScript

typescript, functional-programming, optics, lens, fp-ts

readme

build status dependency status npm downloads

Motivation

(Adapted from monocle site)

Modifying immutable nested object in JavaScript is verbose which makes code difficult to understand and reason about.

Let's have a look at some examples:

interface Street {
  num: number
  name: string
}
interface Address {
  city: string
  street: Street
}
interface Company {
  name: string
  address: Address
}
interface Employee {
  name: string
  company: Company
}

Let’s say we have an employee and we need to upper case the first character of his company street name. Here is how we could write it in vanilla JavaScript

const employee: Employee = {
  name: 'john',
  company: {
    name: 'awesome inc',
    address: {
      city: 'london',
      street: {
        num: 23,
        name: 'high street'
      }
    }
  }
}

const capitalize = (s: string): string => s.substring(0, 1).toUpperCase() + s.substring(1)

const employeeCapitalized = {
  ...employee,
  company: {
    ...employee.company,
    address: {
      ...employee.company.address,
      street: {
        ...employee.company.address.street,
        name: capitalize(employee.company.address.street.name)
      }
    }
  }
}

As we can see copy is not convenient to update nested objects because we need to repeat ourselves. Let's see what could we do with monocle-ts

import { Lens } from 'monocle-ts'

const company = Lens.fromProp<Employee>()('company')
const address = Lens.fromProp<Company>()('address')
const street = Lens.fromProp<Address>()('street')
const name = Lens.fromProp<Street>()('name')

compose takes two Lenses, one from A to B and another one from B to C and creates a third Lens from A to C. Therefore, after composing company, address, street and name, we obtain a Lens from Employee to string (the street name). Now we can use this Lens issued from the composition to modify the street name using the function capitalize

const capitalizeName = company.compose(address).compose(street).compose(name).modify(capitalize)

assert.deepStrictEqual(capitalizeName(employee), employeeCapitalized)

You can use the fromPath API to avoid some boilerplate

import { Lens } from 'monocle-ts'

const name = Lens.fromPath<Employee>()(['company', 'address', 'street', 'name'])

const capitalizeName = name.modify(capitalize)

assert.deepStrictEqual(capitalizeName(employee), employeeCapitalized) // true

Here modify lift a function string => string to a function Employee => Employee. It works but it would be clearer if we could zoom into the first character of a string with a Lens. However, we cannot write such a Lens because Lenses require the field they are directed at to be mandatory. In our case the first character of a string is optional as a string can be empty. So we need another abstraction that would be a sort of partial Lens, in monocle-ts it is called an Optional.

import { Optional } from 'monocle-ts'
import { some, none } from 'fp-ts/Option'

const firstLetterOptional = new Optional<string, string>(
  (s) => (s.length > 0 ? some(s[0]) : none),
  (a) => (s) => (s.length > 0 ? a + s.substring(1) : s)
)

const firstLetter = company.compose(address).compose(street).compose(name).asOptional().compose(firstLetterOptional)

assert.deepStrictEqual(firstLetter.modify((s) => s.toUpperCase())(employee), employeeCapitalized)

Similarly to compose for lenses, compose for optionals takes two Optionals, one from A to B and another from B to C and creates a third Optional from A to C. All Lenses can be seen as Optionals where the optional element to zoom into is always present, hence composing an Optional and a Lens always produces an Optional.

TypeScript compatibility

The stable version is tested against TypeScript 3.5.2, but should run with TypeScript 2.8.0+ too

monocle-ts version required typescript version
2.0.x+ 3.5+
1.x+ 2.8.0+

Note. If you are running < typescript@3.0.1 you have to polyfill unknown.

You can use unknown-ts as a polyfill.

Documentation

Experimental modules (version 2.3+)

Experimental modules (*) are published in order to get early feedback from the community.

The experimental modules are independent and backward-incompatible with stable ones.

(*) A feature tagged as Experimental is in a high state of flux, you're at risk of it changing without notice.

From `monocle@2.3+` you can use the following experimental modules:

  • Iso
  • Lens
  • Prism
  • Optional
  • Traversal
  • At
  • Ix

which implement the same features contained in index.ts but are pipe-based instead of class-based.

Here's the same examples with the new API

interface Street {
  num: number
  name: string
}
interface Address {
  city: string
  street: Street
}
interface Company {
  name: string
  address: Address
}
interface Employee {
  name: string
  company: Company
}

const employee: Employee = {
  name: 'john',
  company: {
    name: 'awesome inc',
    address: {
      city: 'london',
      street: {
        num: 23,
        name: 'high street'
      }
    }
  }
}

const capitalize = (s: string): string => s.substring(0, 1).toUpperCase() + s.substring(1)

const employeeCapitalized = {
  ...employee,
  company: {
    ...employee.company,
    address: {
      ...employee.company.address,
      street: {
        ...employee.company.address.street,
        name: capitalize(employee.company.address.street.name)
      }
    }
  }
}

import * as assert from 'assert'
import * as L from 'monocle-ts/Lens'
import { pipe } from 'fp-ts/function'

const capitalizeName = pipe(
  L.id<Employee>(),
  L.prop('company'),
  L.prop('address'),
  L.prop('street'),
  L.prop('name'),
  L.modify(capitalize)
)

assert.deepStrictEqual(capitalizeName(employee), employeeCapitalized)

import * as O from 'monocle-ts/Optional'
import { some, none } from 'fp-ts/Option'

const firstLetterOptional: O.Optional<string, string> = {
  getOption: (s) => (s.length > 0 ? some(s[0]) : none),
  set: (a) => (s) => (s.length > 0 ? a + s.substring(1) : s)
}

const firstLetter = pipe(
  L.id<Employee>(),
  L.prop('company'),
  L.prop('address'),
  L.prop('street'),
  L.prop('name'),
  L.composeOptional(firstLetterOptional)
)

assert.deepStrictEqual(
  pipe(
    firstLetter,
    O.modify((s) => s.toUpperCase())
  )(employee),
  employeeCapitalized
)

changelog

Changelog

Tags:

  • [New Feature]
  • [Bug Fix]
  • [Breaking Change]
  • [Documentation]
  • [Internal]
  • [Polish]
  • [Experimental]

Note: Gaps between patch versions are faulty/broken releases. Note: A feature tagged as Experimental is in a high state of flux, you're at risk of it changing without notice.

2.3.13

  • Polish
    • improve modify / modifyOption behaviour when using pipe, #181 (@thewilkybarkid)

2.3.12

  • Polish
    • Add missing pure annotations, #175 (@OliverJAsh)

2.3.11

  • Bug Fix
    • OptionalFromPath: Type Issue fix for 5 arguments, #167 (@Barackos)

2.3.10

  • Internal
    • optimize fromTraversable, closes #119 (@gcanti)

2.3.9

Experimental modules require fp-ts@^2.5.0.

  • Experimental
    • At
      • add at constructor (@gcanti)
    • Iso
      • add iso constructor (@gcanti)
      • add composeLens (@gcanti)
      • add composePrism (@gcanti)
      • add composeOptional (@gcanti)
      • add composeTraversal (@gcanti)
      • add fromNullable (@gcanti)
      • add filter (@gcanti)
      • add fromNullable (@gcanti)
      • add prop (@gcanti)
      • add props (@gcanti)
      • add component (@gcanti)
      • add index (@gcanti)
      • add indexNonEmpty (@gcanti)
      • add key (@gcanti)
      • add atKey (@gcanti)
      • add some (@gcanti)
      • add right (@gcanti)
      • add left (@gcanti)
      • add traverse (@gcanti)
      • add findFirst (@gcanti)
      • add findFirstNonEmpty (@gcanti)
      • add composeIso (@gcanti)
      • add Semigroupoid (@gcanti)
      • (*) rename invariantIso to Invariant (@gcanti)
      • (*) rename categoryIso to Category (@gcanti)
    • Ix
      • add index constructor (@gcanti)
      • add indexReadonlyNonEmptyArray (@gcanti)
    • Lens
      • add lens constructor (@gcanti)
      • add composeIso (@gcanti)
      • add composeTraversal (@gcanti)
      • add indexNonEmpty (@gcanti)
      • add findFirstNonEmpty (@gcanti)
      • add composeLens (@gcanti)
      • add Semigroupoid (@gcanti)
      • (*) rename invariantIso to Invariant (@gcanti)
      • (*) rename categoryIso to Category (@gcanti)
    • Prism
      • add prism constructor (@gcanti)
      • add composeIso (@gcanti)
      • add composeTraversal (@gcanti)
      • add indexNonEmpty (@gcanti)
      • add findFirstNonEmpty (@gcanti)
      • add composePrism (@gcanti)
      • add Semigroupoid (@gcanti)
      • (*) rename invariantIso to Invariant (@gcanti)
      • (*) rename categoryIso to Category (@gcanti)
    • Optional
      • add optional constructor (@gcanti)
      • add composeIso (@gcanti)
      • add composeTraversal (@gcanti)
      • add indexNonEmpty (@gcanti)
      • add findFirstNonEmpty (@gcanti)
      • add composeOptional (@gcanti)
      • add Semigroupoid (@gcanti)
      • (*) rename invariantIso to Invariant (@gcanti)
      • (*) rename categoryIso to Category (@gcanti)
    • Traversal
      • add traversal constructor (@gcanti)
      • add composeIso (@gcanti)
      • add composeLens (@gcanti)
      • add composePrism (@gcanti)
      • add composeOptional (@gcanti)
      • add findFirst (@gcanti)
      • add findFirstNonEmpty (@gcanti)
      • add fromNullable (@gcanti)
      • add indexNonEmpty (@gcanti)
      • add composeTraversal (@gcanti)
      • add Semigroupoid (@gcanti)
      • (*) rename categoryIso to Category (@gcanti)

(*) breaking change

2.3.7

  • Experimental
    • At
      • add atReadonlyRecord (@gcanti)
      • add atReadonlyMap (@gcanti)
      • add atReadonlySet (@gcanti)
      • deprecate atRecord (@gcanti)
    • Ix
      • add indexReadonlyRecord (@gcanti)
      • add indexReadonlyArray (@gcanti)
      • add indexReadonlyMap (@gcanti)
      • deprecate indexRecord (@gcanti)
      • deprecate indexArray (@gcanti)
    • Optional
      • add composeLens (@gcanti)
      • add composePrism (@gcanti)
      • add setOption (@gcanti)

2.3.6

  • Polish
    • import without /lib or /es6 paths, closes #147 (@gcanti)

2.3.5

  • Experimental
    • add modifyF, closes #149 (@gcanti)
      • Iso
      • Lens
      • Prism
      • Optional

2.3.4

  • Experimental
    • Lens, Prism, Optional: add Refinement overload to findFirst, #148 (@wmaurer)

2.3.3

  • Experimental
    • Prism
      • (*) remove fromSome constructor (@gcanti)
      • (*) change fromNullable signature (@gcanti)
    • Optional
      • add missing fromNullable combinator, closes #133 (@gcanti)

(*) breaking change

2.3.2

  • Experimental
    • At
      • add atRecord (@gcanti)
    • Ix
      • add indexArray (@gcanti)
      • add indexRecord (@gcanti)
    • Lens
      • add findFirst combinator, closes #131 (@gcanti)
    • Prism
      • add findFirst combinator (@gcanti)
      • add traverse combinator (@gcanti)
    • Optional
      • add findFirst combinator (@gcanti)
      • add traverse combinator (@gcanti)

2.3.1

  • Experimental
    • add Iso module (@gcanti)
    • add Lens module (@gcanti)
    • add Prism module (@gcanti)
    • add Optional module (@gcanti)
    • add Traversal module (@gcanti)
    • add At module (@gcanti)
    • add Ix module (@gcanti)
  • Internal
    • implement old APIs through new APIs (@gcanti)

2.2.0

  • New Feature
    • At
      • add ReadonlyRecord module (@gcanti)
      • add ReadonlySet module (@gcanti)
    • Index
      • add ReadonlyArray module (@gcanti)
      • add ReadonlyNonEmptyArray module (@gcanti)
      • add ReadonlyRecord module (@gcanti)

2.1.1

  • Bug Fix
    • fix Optional.fromPath, #122 (@mikearnaldi)

2.1.0

  • New Feature
    • Add support for Optional.fromPath, #105 (@cybermaak)
  • Bug Fix
    • handle nullable values in fromNullableProp, fix #106 (@gcanti)

2.0.1

  • Bug Fix
    • rewrite es6 imports (@gcanti)

2.0.0

  • Breaking Change
    • upgrade to `fp-ts@2.x` (@gcanti)
    • remove deprecated APIs (@gcanti)
      • uncurried Lens.fromProp
      • uncurried Lens.fromProps
      • uncurried Lens.fromNullableProp
      • uncurried Optional.fromNullableProp
      • uncurried Optional.fromOptionProp
    • remove At/StrMap (@gcanti)
    • remove Index/StrMap (@gcanti)

1.7.2

output ES modules to better support tree-shaking (@gcanti)

1.7.1

  • Polish
    • move fp-ts to peerDependencies (@gcanti)

1.7.0

  • New Feature
    • add At/Record (@mlegenhausen)
    • add Index/Record (@mlegenhausen)

1.6.1

  • Polish
    • Providing a Refinement to the find method of Fold will return the narrowed type (@Stouffi)

1.6.0

  • New Feature
    • add Traversal.prototype.filter (@gcanti)
    • add Either prisms (@gcanti)
  • Polish
    • many optimizitions (@sledorze)
  • Deprecation
    • deprecate Prism.fromRefinement in favour of Prism.fromPredicate (@gcanti)

1.5.3

  • Bug Fix
    • revert 27b587b, closes #75 (@gcanti)

1.5.2

  • Polish
    • disallow improper use of from* APIs, closes #73 (@gcanti)

1.5.1

  • Polish
    • add aliases for compose methods, closes #51 (@gcanti)

1.5.0

  • New Feature
    • add indexNonEmptyArray (@sledorze)
  • Internal

Note. If you are running < typescript@3.0.1 you have to polyfill unknown.

You can use unknown-ts as a polyfill.

1.4.1

  • New Feature
    • add Prism.fromRefinement (@bepremeg)
    • add Optional.fromOptionProp (@bepremeg)

1.3.0

  • New Feature
    • add Lens.{fromProp, fromPath, fromNullableProp} and Optional.fromNullableProp curried overloadings (@gcanti)

1.2.0

  • New Feature
    • add At (@leighman)
      • add Set instance
      • add StrMap instance
    • add Index (@leighman)
      • add Array instance
      • add StrMap instance

1.1.0

  • New Feature
    • add Lens.fromProps (@gcanti)

1.0.0

0.5.2

  • New Feature
    • add Iso.reverse, closes #36 (@gcanti)

0.5.1

  • Experimental
    • add Flowtype support (@gcanti)

0.5.0

  • Breaking Change
    • upgrade to fp-ts 0.6 (@gcanti)

0.4.4

  • New Feature
    • add Lens.fromNullableProp (@gcanti)

0.4.3

  • New Feature
    • Prism: add set method (@gcanti)
    • Optional: add fromNullableProps static function (@gcanti)
  • Bug fix
    • Prism: change asOptional, asTraversal implementations, fix #29 (@gcanti)

0.4.2

  • Polish
    • fix Optional laws (@gcanti)
    • remove Endomorphism type alias (@gcanti)

0.4.1

  • New Feature
    • Add aliases to ISO, closes #24 (@gcanti)

0.4.0

  • Breaking Change
    • upgrade to fp-ts 0.5 (@gcanti)
    • currying of all APIs (@gcanti)

0.3.2

  • Polish
    • upgrade to latest fp-ts (0.4.3) (@gcanti)

0.3.1

  • New Feature
    • Added Setter (@LiamGoodacre)
    • Added Getter (@LiamGoodacre)
    • Added all possible conversions (e.g asGetter, asFold, etc) (@LiamGoodacre)
    • Added all possible compositions (@LiamGoodacre)
    • add _tag fields (allows for tagged unions) (@gcanti)
  • Polish
    • Fixed some typos (Options/Option -> Optional) (@LiamGoodacre)
    • Minor rearrangement so conversions and compositions are grouped (@LiamGoodacre)

0.3.0

  • Breaking Change
    • upgrade to latest fp-ts (@gcanti)

0.2.0

  • New Feature
    • add Prism.some, closes #10 (@gcanti)
    • add composeX, closes #11 (@gcanti)
  • Breaking Change
    • upgrade to latest fp-ts (monocle-ts APIs are not changed though) (@gcanti)
    • drop lib-jsnext folder
    • remove Optional.fromProp, closes #9 (@gcanti)

0.1.1

  • New Feature
    • add Prism.fromPredicate
    • fix Optional.fromProp

0.1.0

Initial release