This content originally appeared on DEV Community and was authored by Anthony G
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)
Change Object types
import { upsert } from 'spectacles-ts'
const objS: { a: { b: string} } = pipe(
obj,
upsert(['a', 'b'], 'abc')
)
Also featuring modifyW (which stands for 'modify widen' - as in 'widen the output type')
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:
const as: number[] = [{ a: 123 }].map(get('a'))
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'
// |
// v
const a: O.Option<number> = pipe([{ a: 123 }], get(0, 'a'))
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
Also featuring modifyOption and modifyOptionW
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
declare const rec: Record<string, { a: number }>
const a: number[] = pipe(rec, get('{}>', 'a'))
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: Record<string, number> = { a: 123 }
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 } }
Or remove 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' } }
You can rename a key:
import { rename } from 'spectacles-ts'
const renamedKey: { nest: { a2: number } } = pipe(
{ nest: { a: 123 } },
remove(['nest', 'a2'])
)
// renamedKey = { nest: { a2: 123 } }
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
This content originally appeared on DEV Community and was authored by Anthony G
Anthony G | Sciencx (2021-08-25T04:04:46+00:00) Simple Immutable Data w/ Spectacles ?. Retrieved from https://www.scien.cx/2021/08/25/simple-immutable-data-w-spectacles-%f0%9f%91%93/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.