An Interest In:
Web News this Week
- April 27, 2024
- April 26, 2024
- April 25, 2024
- April 24, 2024
- April 23, 2024
- April 22, 2024
- April 21, 2024
Simple Immutable Data w/ Spectacles
Do you want to love immutable data but think it's a drag?
Are you perplexed by the syntax of immutability-helper? Repulsed by immer.js's use of assignment? Alarmed by lodash's lack of type safety?
Looking for something a little more intuitive, powerful & flexible? Get some clarity w/ spectacles (github repo)!
Syntax (featuring auto-complete!)
import { pipe } from 'fp-ts'import { set } from 'spectacles-ts'const oldObj = { a: { b: 123 } }const newObj = pipe(oldObj, set(['a', 'b'], 999))// oldObj = { a: { b: 123 } }// newObj = { a: { b: 999 } }
It's that simple!
(If pipe
syntax is unfamiliar checkout this quick explanation)
Functional Programming (fp)
spectacles-ts
integrates seamlessly with the fp-ts ecosystem (it's built on top of the excellent monocle-ts library)
Its curried functions fit in nicely w/ a functional style
That's one reason you might want to use a function like get
:
import { get } from 'spectacles-ts'const as: number[] = [{ a: 123 }].map(get('a'))// as = [123]
Array access
We can do Array
access using a number
for the index:
const a = pipe([{ a: 123 }], get(0, 'a'))
Since Array
access at a given index might fail, we use fp-ts's Option
type
import * as O from 'fp-ts/Option'// |// vconst a: O.Option<number> = pipe([{ a: 123 }], get(0, 'a'))// a = O.some(123)
The Option
type is powerful, featuring a full set of combinators. It can be a great, simple intro into the joys of fp-ts
This also gives us a way to know when a 'set' call has failed, using setOption
:
import { set, setOption } from 'spectacles-ts'const silentSuccess: number[] = pipe([123], set([0], 999))const silentFailure: number[] = pipe([123], set([1], 999))// silentSuccess = [999]// silentFailure = [123]const noisySuccess: O.Option<number[]> = pipe([123], setOption([0], 999))const noisyFailure: O.Option<number[]> = pipe([123], setOption([1], 999))// noisySuccess = O.some([999])// noisyFailure = O.none
Traversals
We can traverse an Array
to collect its nested data
const a: number[] = pipe( [{ a: 123 }, { a: 456 }], get('[]>', 'a'))// equivalent to:const a2: number[] = [{ a: 123 }, { a: 456 }].map(get('a'))// a = a2 = [123, 456]
Or to make a change across all of its values
const a: { a: number }[] = pipe( [{ a: 123 }, { a: 456 }], set(['[]>', 'a'], 999))// a = [{ a: 999 }, { a: 999 }]
We can also traverse a Record
const rec = { a: 123, b: 456 } as Record<string, { a: number }>const a: number[] = pipe(rec, get('{}>', 'a'))// a = [123, 456]
Modification
You can modify a value in relation to its old value:
import { modify } from 'spectacles-ts'const mod: { a: number }[] = pipe([{ a: 123 }], modify([0, 'a'], a => a + 4))// mod = [{ a: 127 }]
You can even change a value's type this way:
import { modifyW } from 'spectacles-ts'// ^// |// The 'W' stands for 'widen'// as in 'widen the type'const modW: { a: string | number }[] = pipe([{ a: 123 }], modifyW([0, 'a'], a => `${a + 4}`))// mod = { a: '127' }
Also featuring modifyOption and modifyOptionW
Change Object types
You can change an existing key:
import { upsert } from 'spectacles-ts'const obj: { a: { b: string} } = pipe( { a: { b: 123 } }, upsert(['a', 'b'], 'abc'))// obj = { a: { b: 'abc' } }
Or add a new one:
const obj: { a: { b: number; c: string } } = pipe( { a: { b: 123 } }, upsert(['a', 'c'], 'abc'))// obj = { a: { b: 123, c: 'abc' } }
Or remove a few of them:
import { remove } from 'spectacles-ts'const removedKeys: { nest: { b: string } } = pipe( { nest: { a: 123, b: 'abc', c: false } }, remove('nest', ['a', 'c'] as const))// removedKeys = { nest: { b: 'abc' } }
Or rename a key:
import { rename } from 'spectacles-ts'const renamedKey: { nest: { a2: number } } = pipe( { nest: { a: 123 } }, rename(['nest', 'a'], 'a2'))// renamedKey = { nest: { a2: 123 } }
Other stuff
You can access the index of a tuple:
const tup = [123, 'abc'] as [number, string]const getIndex: number = pipe(tup, get('0'))// getIndex = 123
You can access the key of a record:
const rec = { a: 123 } as Record<string, number>const getKey = pipe(rec, get('?key', 'a'))// getKey = 123
You can pick a few keys:
const pickedKeys = pipe( { nest: { a: 123, b: 'abc', c: false } }, set(['nest', ['a', 'c'] as const], { a: 999, c: true }))// pickedKeys = { nest: { a: 999, b: 'abc', c: true } }
You can refine a union type:
const refined: number = pipe( { a: 123 } as { a: string | number }, get('a', (a): a is number => typeof a === 'number'))
And there are convenience operations for working with Option
and Either types
Limitation
You can only use up to four operations at a time (Alas!)
You can nest functions instead:
import { pipe } from 'fp-ts/function'import { get, set, modify } from 'spectacles-ts'const getDeep: number = pipe( { a: { b: { c: { d: { e: 123 } } } } }, get('a', 'b', 'c', 'd'), get('e'))const setDeep = pipe( { a: { b: { c: { d: { e: 123 } } } } }, modify( ['a', 'b', 'c', 'd'], set(['e'], 321) ))
Nesting functions that change their output type looks a little uglier, but it works:
const upsertDeep: { a: { b: { c: { d: { e: number; e2: string } } } } } = pipe( { a: { b: { c: { d: { e: 123 } } } } }, modifyW( ['a', 'b', 'c', 'd'], val => pipe( val, upsert(['e2'], 'abc') ) ))
spectacles-ts
vs monocle-ts
spectacles-ts
is built on top of monocle-ts, which is more powerful and flexible but a little less ergonomic.
monocle-ts has these advantages:
spectacles-ts
only works in piped contexts (except for get)- No limitation on object size
- can filter (similar to es6's filter)
- can traverse on any arbitrary traversable object (aka Zippers or Rose Trees)
- Can define an isomorphism between two objects
- works with the Map type
Conclusion
I hope spectacles-ts can help you modify data both immutably & ergonomically!
CREDITS:
Logo - Stuart Leach
Original Link: https://dev.to/anthonyjoeseph/simple-immutable-data-w-spectacles-4nb5
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To