diff --git a/README.md b/README.md index 97488c2..536f244 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,7 @@ A facade on top of [monocle-ts](https://github.com/gcanti/monocle-ts) - [`modifyOptionW`](#modifyoptionw) - [`modifyF`](#modifyf) - [Operations](#operations) -- [Limitation](#limitation) -- [TSC Issues](#tsc-issues) +- [Social Media](#social-media) ## Installation @@ -60,10 +59,11 @@ const nested: O.Option = pipe( import { pipe } from 'fp-ts/function' import { set } from 'spectacles-ts' -const beenSet: { a: string[] } = pipe( +const beenSet = pipe( { a: ['abc', 'def'] }, - set(['a', 1], 'xyz') + set('a.[number]', 1, 'xyz') ) +// beenSet: { a: string[] } ``` ### `setOption` @@ -74,10 +74,11 @@ const beenSet: { a: string[] } = pipe( import { pipe } from 'fp-ts/function' import { setOption } from 'spectacles-ts' -const beenSet: O.Option<{ a: string[] }> = pipe( +const beenSet = pipe( { a: ['abc', 'def'] }, - setOption(['a', 1], 'xyz') + setOption('a.[number]', 1, 'xyz') ) +// beenSet: O.Option<{ a: string[] }> ``` ### `upsert` @@ -88,10 +89,11 @@ const beenSet: O.Option<{ a: string[] }> = pipe( import { pipe } from 'fp-ts/function' import { upsert } from 'spectacles-ts' -const upsertKey: { a: { b: number; readonly c: string } } = pipe( +const upsertKey = pipe( { a: { b: 123 } }, - upsert(['a', 'c'], 'abc') + upsert('a', 'c', 'abc') ) +// upsertKey: { a: { b: number; readonly c: string } } ``` ### `remove` @@ -100,10 +102,11 @@ const upsertKey: { a: { b: number; readonly c: string } } = pipe( import { pipe } from 'fp-ts/function' import { remove } from 'spectacles-ts' -const removeKey: { a: { b: number } } = pipe( +const removeKey = pipe( { a: { b: 123, c: false } }, - remove('a', 'c') + remove('a.c') ) +// removeKey: { a: { b: number } } ``` ### `rename` @@ -114,10 +117,11 @@ import type { NonEmptyArray } from 'fp-ts/NonEmptyArray' import type { Option } from 'fp-ts/Option' import { rename } from 'spectacles-ts' -const rename: { a: { readonly newKey: number } } = pipe( +const rename = pipe( { a: { oldKey: 123 } }, - rename(['a', 'oldKey'], 'newKey') + rename('a.oldKey', 'newKey') ) +// rename: { a: { readonly newKey: number } } ``` ### `modify` @@ -130,10 +134,11 @@ const rename: { a: { readonly newKey: number } } = pipe( import { pipe } from 'fp-ts/function' import { modify } from 'spectacles-ts' -const modifyOpted: { a: { b: number }[] } = pipe( +const modified = pipe( { a: [{ b: 123 }] }, - modify(['a', 0, 'b'], (j) => j + 4) + modify('a.[number].b', 0, (j) => j + 4) ) +// modified: { a: { b: number }[] } ``` ### `modifyOption` @@ -145,10 +150,11 @@ import { pipe } from 'fp-ts/function' import * as O from 'fp-ts/Option' import { modifyOption } from 'spectacles-ts' -const modifyOpted: O.Option<{ a: { b: number }[] }> = pipe( +const modifyOpted = pipe( { a: [{ b: 123 }] }, - modifyOption(['a', 0, 'b'], (j) => j + 4) + modifyOption('a.[number].b', 0, (j) => j + 4) ) +// modifyOpted: O.Option<{ a: { b: number }[] }> ``` ### `modifyW` @@ -170,10 +176,11 @@ import { pipe } from 'fp-ts/function' import * as O from 'fp-ts/Option' import { modifyOptionW } from 'spectacles-ts' -const modified: O.Option<{ a: string | undefined }> = pipe( +const modified = pipe( { a: 123 } as { a: number | undefined }, - modifyOptionW(['a', '?'], (j) => `${j + 2}`) + modifyOptionW('a.?', (j) => `${j + 2}`) ) +// modified: O.Option<{ a: string | undefined }> ``` ### `modifyF` @@ -185,13 +192,14 @@ import { pipe } from 'fp-ts/function' import * as E from 'fp-ts/Either' import { modifyF } from 'spectacles-ts' -const modified: E.Either = pipe( +const modified = pipe( { a: { b: 123 } }, modifyF(E.Applicative)( ['a', 'b'], - (j) => j > 10 ? E.left('fail') : E.right(j - 10) + (j) => j > 10 ? E.left('fail') : E.right(j - 10) ) ) +// modified: E.Either ``` ## Operations @@ -199,17 +207,16 @@ const modified: E.Either = pipe( | usage       | equals | Optional | monocle | |------|-----|-------|-------| | `get('a')(x)`| `1` | no | [prop](https://github.com/gcanti/monocle-ts/blob/master/test/Lens.ts#L89) | -| `get(['a', 'b'] as const)(x)` | `{ a: 1, b: 2 }` | no | [props](https://github.com/gcanti/monocle-ts/blob/master/test/Lens.ts#L103) | -| `get('c', '0')(x)`| `123` | no | [component](https://github.com/gcanti/monocle-ts/blob/master/test/Lens.ts#L119) -| `get('d', 0)(x)`| `O.some({ e: 123 })` | yes | [index](https://github.com/gcanti/monocle-ts/blob/master/test/Optional.ts#L107) -| `get('f', '?key', 'a')(x)` | `O.some([123])` | yes | [key](https://github.com/gcanti/monocle-ts/blob/master/test/Optional.ts#L133) | -| `get('g', '?')(x)` | `O.some(2)` | yes | [fromNullable](https://github.com/gcanti/monocle-ts/blob/master/test/Optional.ts#L223) | -| `get('h', '?some')(x)` | `O.some(2)` | yes | [some](https://github.com/gcanti/monocle-ts/blob/master/src/Optional.ts#L287) -| `get('i', '?left')(x)`| `O.none` | yes | [left](https://github.com/gcanti/monocle-ts/blob/master/test/Prism.ts#L200) -| `get('i', '?right')(x)`| `O.some(2)` | yes | [right](https://github.com/gcanti/monocle-ts/blob/master/test/Prism.ts#L192) -| `get('j', (a): a is number => typeof a === 'number')(x)`| `O.some(2)` | yes | [filter](https://github.com/gcanti/monocle-ts/blob/master/test/Optional.ts#L160) -| `get('d', '[]>', 'e')(x)` | `[123, 456]` | never | [traverse](https://github.com/gcanti/monocle-ts/blob/master/test/Optional.ts#L215)
`Array` | -| `get('f', '{}>', 0)(x)` | `[123, 456]` | never | `traverse`
`Record`
(keys sorted alpha-
betically) | +| `get('c.[0]')(x)`| `123` | no | [component](https://github.com/gcanti/monocle-ts/blob/master/test/Lens.ts#L119) +| `get('d.[number]', 0)(x)`| `O.some({ e: 123 })` | yes | [index](https://github.com/gcanti/monocle-ts/blob/master/test/Optional.ts#L107) +| `get('f.[string]', 'a')(x)` | `O.some([123])` | yes | [key](https://github.com/gcanti/monocle-ts/blob/master/test/Optional.ts#L133) | +| `get('g.?')(x)` | `O.some(2)` | yes | [fromNullable](https://github.com/gcanti/monocle-ts/blob/master/test/Optional.ts#L223) | +| `get('h.?some')(x)` | `O.some(2)` | yes | [some](https://github.com/gcanti/monocle-ts/blob/master/src/Optional.ts#L287) +| `get('i.?left')(x)`| `O.none` | yes | [left](https://github.com/gcanti/monocle-ts/blob/master/test/Prism.ts#L200) +| `get('i.?right')(x)`| `O.some(2)` | yes | [right](https://github.com/gcanti/monocle-ts/blob/master/test/Prism.ts#L192) +| `get('j.shape:circle.radius')(x)`| `O.some(100)` | yes | [filter](https://github.com/gcanti/monocle-ts/blob/master/test/Optional.ts#L160) +| `get('d.[]>.e')(x)` | `[123, 456]` | never | [traverse](https://github.com/gcanti/monocle-ts/blob/master/test/Optional.ts#L215)
`Array` | +| `get('f.{}>.e')(x)` | `[123, 456]` | never | `traverse`
`Record`
(keys sorted alpha-
betically) | ```ts import * as O from 'fp-ts/Option' @@ -223,65 +230,21 @@ interface Data { g?: number h: O.Option i: E.Either - j: number | string + j: { shape: "circle"; radius: number } | { shape: "rectangle"; width: number; height: number } } const x: Data = { a: 1, b: 2, c: [123, 'abc'], d: [{ e: 123 }, { e: 456 }], - f: { b: [456], a: [123] }, + f: { b: { e: 456 }, a: { e: 123 } }, g: 2, h: O.some(2), i: E.right(2), - j: 2 + j: { shape: "circle", radius: 100 } } ``` -## Limitation +## Social Media -You can only use up to four (4) operations at a time - -Allowing any more could cause [tsc errors](https://stackoverflow.com/questions/57798016/how-to-ignore-type-instantiation-is-excessively-deep-and-possibly-infinite-ts) - -You can nest functions instead: - -```ts -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 atm (it's an [open issue](https://github.com/anthonyjoeseph/spectacles-ts/issues/4)): - -```ts -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') - ) - ) -) -``` - -## TSC Issues - -Please give this issue an upvote! It would help w/ autocomplete for this library - -- [Restrict the intellisense/auto completion of mapped tuples depending on the first element of the tuple](https://github.com/microsoft/TypeScript/issues/43824) +Follow me on twitter! [@typesafeFE](https://twitter.com/typesafeFE) \ No newline at end of file diff --git a/blog-post.md b/blog-post.md index 6fe7cb4..0c833c4 100644 --- a/blog-post.md +++ b/blog-post.md @@ -6,24 +6,6 @@ Are you perplexed by the syntax of [immutability-helper](https://github.com/kolo Looking for something a little more intuitive, powerful & flexible? Clear up your data w/ `spectacles-ts` ([github repo](https://github.com/anthonyjoeseph/spectacles-ts))! -## IDEAS - -## setOption as motivation for Option - -let's say we want to clear out the value of 'a' - -```ts -declare const data: { a: number } | undefined -const f = pipe(data, setOption('?.a.?', 2)) -// f: Option<{ a: number } | undefined> -``` - -if f is `Some`, then we know that `data.a` had previously contained a number. If it's `None`, that means that `data.a` had contained 'undefined' - -if we used `undefined`, 'f' would have the type `number | undefined` - -this would leave us no way to know if the operation had failed, or if - ## Installation ```shell @@ -33,117 +15,59 @@ yarn add fp-ts spectacles-ts ## Syntax (featuring [auto-complete](https://github.com/anthonyjoeseph/spectacles-ts/blob/main/readme-vid.gif)!) ```ts -import { pipe } from 'fp-ts' +import { pipe } from 'fp-ts/function' import { set } from 'spectacles-ts' const oldObj = { a: { b: 123 } } -const newObj = pipe(oldObj, set(['a', 'b'], 999)) +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](https://rlee.dev/practical-guide-to-fp-ts-part-1)) - -You can `get` a value with similar syntax: - -```ts -import { get } from 'spectacles-ts' +## pipe -const num: number = pipe({ a: { b: 123 } }, get('a', 'b')) +You might be wondering what that function called `pipe` is for -// equivalent to -const num2: number = { a: { b: 123 } }.a.b - -// num = num2 = 123 -``` - -## Functional Programming (fp) - -`spectacles-ts` integrates seamlessly with the [fp-ts ecosystem](https://gcanti.github.io/fp-ts/ecosystem/) (it's built on top of the excellent [monocle-ts](https://github.com/gcanti/monocle-ts) library) - -Its [curried functions](https://paulgray.net/pipeable-apis/) fit in nicely w/ a functional style - -That's one reason you might want to use a function like `get`: +It can simplify the use of many nested functions ```ts -const as: number[] = [{ a: 123 }].map(get('a')) -// as = [123] -``` - -## Array access - -We can do `Array` access using a `number` for the index: - -```ts -const a = pipe([{ a: 123 }], get(0, 'a')) -``` - -Since `Array` access at a given index might fail, we use fp-ts's `Option` type - -```ts -import * as O from 'fp-ts/Option' +import { pipe } from 'fp-ts/function' -// | -// v -const a: O.Option = pipe([{ a: 123 }], get(0, 'a')) -// a = O.some(123) +const manyfuncs = String(Math.floor(Number.parseFloat("123.456"))); +const samething = pipe( + "123.456", + Number.parseFloat, + Math.floor, + String +); ``` -The `Option` type is powerful, featuring a [full set of combinators](https://rlee.dev/practical-guide-to-fp-ts-part-2). 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`: - -```ts -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] +It's a bit easier to read, and type-safe -const noisySuccess: O.Option = pipe([123], setOption([0], 999)) -const noisyFailure: O.Option = pipe([123], setOption([1], 999)) -// noisySuccess = O.some([999]) -// noisyFailure = O.none -``` - -## Traversals +At the end, I'll explain why `spectacles-ts` uses `pipe` -We can traverse an `Array` to collect its nested data +# Whats fp-ts -```ts -const a: number[] = pipe( - [{ a: 123 }, { a: 456 }], - get('[]>', 'a') -) +You might have noticed a few references to the npm package called [fp-ts](https://www.npmjs.com/package/fp-ts). It's the latest in the line of successon of data utility libraries for javascript -// equivalent to: -const a2: number[] = [{ a: 123 }, { a: 456 }].map(get('a')) +[underscore.js](https://underscorejs.org/) -> [lodash](https://lodash.com/) -> [ramda](https://ramdajs.com/) -> [fantasy land](https://github.com/fantasyland/fantasy-land) -> [fp-ts](https://github.com/gcanti/fp-ts) -// a = a2 = [123, 456] -``` +`fp-ts` stands for 'functional programming in typescript'. 'Functional programming' just means that it helps with data transformations -Or to make a change across all of its values +Usually functions from `fp-ts` and its [libraries](https://gcanti.github.io/fp-ts/ecosystem/) (including `spectacles-ts`) rely on `pipe` -```ts -const a: { a: number }[] = pipe( - [{ a: 123 }, { a: 456 }], - set(['[]>', 'a'], 999) -) +# What else can spectacles do -// a = [{ a: 999 }, { a: 999 }] -``` +## Tuples -We can also traverse a `Record`. The keys are sorted alphabetically +You can access the index of a tuple: ```ts -const rec = - { two: { a: 456 }, one: { a: 123 } } as Record -const a: number[] = pipe(rec, get('{}>', 'a')) - -// a = [123, 456] +const tup = [123, 'abc'] as [number, string] +const getIndex: number = pipe(tup, set('[0]', 456)) +// getIndex = [456, 'abc'] ``` ## Modification @@ -153,8 +77,9 @@ You can modify a value in relation to its old value: ```ts import { modify } from 'spectacles-ts' -const mod: { a: number }[] = - pipe([{ a: 123 }], modify([0, 'a'], a => a + 4)) +const mod = + pipe([{ a: 123 }], modify('[number].a', 0, a => a + 4)) +// mod: { a: number }[] // mod = [{ a: 127 }] ``` @@ -163,10 +88,11 @@ You can use this to e.g. append to an array: ```ts import * as A from 'fp-ts/ReadonlyArray' -const app: { a: number[] } = pipe( +const app = pipe( { a: [123] }, - modify(['a'], A.append(456)) + modify('a', A.append(456)) ) +// app: { a: number[] } // app = { a: [123, 456] } ``` @@ -179,12 +105,39 @@ 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' } +const modW = + pipe([{ a: 123 }, { a: 456 }], modifyW('[number].a', 0, a => `${a + 4}`)) +// modW: { a: string | number }[] +// modW = [{ a: '127' }, { a: 456 }] ``` -Also featuring [modifyOption](https://github.com/anthonyjoeseph/spectacles-ts#modifyoption) and [modifyOptionW](https://github.com/anthonyjoeseph/spectacles-ts#modifyoptionw) +## Traversals + +We can traverse an `Array` to collect its nested data + +```ts +const a = pipe( + [{ a: 123 }, { a: 456 }], + set('[]>.a', 999) +) + +// equivalent to: +const a2 = [{ a: 123 }, { a: 456 }].map(set('a', 999)) + +// a: { a: number }[] +// a2: { a: number }[] +// a = a2 = [{ a: 999 }, { a: 999 }] +``` + +We can also traverse a `Record` + +```ts +const rec = + { two: { a: 456 }, one: { a: 123 } } as Record +const a = pipe(rec, set('{}>.a', 999)) +// a: Record +// a = { one: { a: 999 }, two: { a: 999 } } +``` ## Change Object types @@ -193,34 +146,36 @@ You can change an existing key: ```ts import { upsert } from 'spectacles-ts' -const obj: { a: { b: string} } = pipe( +const obj = pipe( { a: { b: 123 } }, upsert(['a', 'b'], 'abc') ) +// obj: { a: { b: string} } // obj = { a: { b: 'abc' } } ``` Or add a new one: ```ts - -const obj: { a: { b: number; c: string } } = pipe( +const obj = pipe( { a: { b: 123 } }, upsert(['a', 'c'], 'abc') ) +// obj: { a: { b: number; c: string } } // obj = { a: { b: 123, c: 'abc' } } ``` -Or remove a few of them: +Or remove one of them: ```ts import { remove } from 'spectacles-ts' -const removedKeys: { nest: { b: string } } = pipe( +const removedKeys = pipe( { nest: { a: 123, b: 'abc', c: false } }, - remove('nest', ['a', 'c'] as const) + remove('nest.a') ) -// removedKeys = { nest: { b: 'abc' } } +// removedKeys: { nest: { b: string, c: boolean } } +// removedKeys = { nest: { b: 'abc', c: false } } ``` Or rename a key: @@ -228,107 +183,164 @@ Or rename a key: ```ts import { rename } from 'spectacles-ts' -const renamedKey: { nest: { a2: number } } = pipe( +const renamedKey = pipe( { nest: { a: 123 } }, - rename(['nest', 'a'], 'a2') + rename('nest', 'a', 'a2') ) +// renamedKey: { nest: { a2: number } } // renamedKey = { nest: { a2: 123 } } ``` -## Other stuff +## Array access -You can access the index of a tuple: +We can do `Array` access using a `number` for the index: ```ts -const tup = [123, 'abc'] as [number, string] -const getIndex: number = pipe(tup, get('0')) -// getIndex = 123 +const array: { a: number }[] = [{ a: 123 }] +const a = array[0].a +const a2 = pipe(array, get('[number].a', 0)) +// ^ +// The index '0' comes after the path string '[number].a' ``` -You can pick a few keys: +Since `Array` access at a given index might fail, we use fp-ts's `Option` type ```ts -const pickedKeys: { a: number; c: boolean } = pipe( - { nest: { a: 123, b: 'abc', c: false } }, - get(['nest', ['a', 'c'] as const]) -) -// pickedKeys = { a: 123, c: true } +import * as O from 'fp-ts/Option' + +// | +// v +const a2: O.Option = pipe(array, get('[number].a', 0)) +// a = O.some(123) +``` + +This also gives us a way to know when a 'set' call has failed, using `setOption`: + +```ts +import { set, setOption } from 'spectacles-ts' + +const silentSuccess = pipe([123], set('[number]', 0, 999)) +const silentFailure = pipe([123], set('[number]', 1, 999)) +// silentSuccess: number[] +// silentFailure: number[] +// silentSuccess = [999] +// silentFailure = [123] + +const noisySuccess = pipe([123], setOption('[number]', 0, 999)) +const noisyFailure: O.Option = pipe([123], setOption('[number]', 1, 999)) +// noisySuccess: O.Option +// noisyFailure: O.Option +// noisySuccess = O.some([999]) +// noisyFailure = O.none ``` +Also featuring [modifyOption](https://github.com/anthonyjoeseph/spectacles-ts#modifyoption) and [modifyOptionW](https://github.com/anthonyjoeseph/spectacles-ts#modifyoptionw) + +# Whats Option + +The `Option` type is a useful alternative to `undefined` because it can nest + +Consider the following problem: + +```ts +const usernames: (string | undefined)[] = ["anthony", undefined, "stu"] +const atindex = usernames[1] +// atindex = undefined +``` + +We know that `atindex` is `undefined`, but we don't know what that means + +It could be `undefined` because the user chose to remain anonymous. It could also be `undefined` because the user doesn't exist at all + +`Option` gives us a way out + +```ts +import { Option } from 'fp-ts/Option' +import { lookup } from 'fp-ts/ReadonlyArray' +const usernames: Option[] = [O.some("anthony"), O.none, O.some("stu")] +const atindex: Option> = pipe(usernames, lookup(1)) +// atindex = O.some(O.none) +``` + +`atindex = O.some(O.none)` means that the user exists and is anonymous. `atindex = O.none` means that the user doesn't exist in the first place + +For this reason `Option` should generally be used instead of `undefined` + +The `Option` type is powerful, featuring a [full set of combinators](https://rlee.dev/practical-guide-to-fp-ts-part-2), far more so than `undefined`. They can `map` and `flatten`, just like arrays and objects + +`Option` can be a great, simple intro into the joys of `fp-ts` + +# OK what else can spectacles do with Option + +## Nullables + You can access a [nullable value](https://www.typescriptlang.org/docs/handbook/advanced-types.html#nullable-types): ```ts interface Obj { a?: { b: number } } const obj: Obj = { a: { b: 123 } } -const a: O.Option = pipe(a, get('a', '?', 'b')) +const a = pipe(a, get('a?.b')) +// a: O.Option // a = O.some(123) ``` +## Other stuff + You can access a key of a record: ```ts const rec = { a: 123 } as Record -const getKey: O.Option = pipe(rec, get('?key', 'a')) +const getKey = pipe(rec, get('[string].a')) +// getKey: O.Option // getKey = O.some(123) ``` -You can refine a union type: +You can refine a discriminated union: ```ts -const refined: O.Option = pipe( - { a: 123 } as { a: string | number }, - get('a', (a): a is number => typeof a === 'number') +type Shape = { shape: "circle"; radius: number } | { shape: "rectangle"; width: number; height: number } +const refined = pipe( + { shape: "circle"; radius: 123 } as Shape, + get('shape:circle.radius') ) +// refined: O.Option // refined = O.some(123) ``` And there are [convenience](https://github.com/anthonyjoeseph/spectacles-ts#operations) operations for working with `Option` and [Either](https://rlee.dev/practical-guide-to-fp-ts-part-3) types -## Limitation - -You can only use up to four operations at a time (Alas!) +## get -You can nest functions instead: +You can `get` a value with similar syntax: ```ts -import { pipe } from 'fp-ts/function' -import { get, set, modify } from 'spectacles-ts' +import { get } from 'spectacles-ts' -const getDeep: number = pipe( - { a: { b: { c: { d: { e: 123 } } } } }, - get('a', 'b', 'c', 'd'), - get('e') -) +const num = pipe({ a: { b: 123 } }, get('a.b')) +// num: number +// num = 123 -const setDeep = pipe( - { a: { b: { c: { d: { e: 123 } } } } }, - modify( - ['a', 'b', 'c', 'd'], - set(['e'], 321) - ) -) +// equivalent to +const num2 = { a: { b: 123 } }.a.b +// num2: number +// num2 = 123 ``` -Nesting functions that change their output type looks a little uglier, but it works: +The [curried functions](https://javascript.info/currying-partials) from `spectacles-ts` fit in nicely w/ a functional style + +That's one reason you might want to use a function like `get`: ```ts -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') - ) - ) -) +const as = [{ a: 123 }].map(get('a')) +// as: number[] +// as = [123] ``` ## `spectacles-ts` vs `monocle-ts` `spectacles-ts` is built on top of [monocle-ts](https://github.com/gcanti/monocle-ts), which is more powerful and flexible but a little less ergonomic. -Here's a direct comparison between the two. +Here's a side-by-side comparison between the two. ```ts import { pipe } from 'fp-ts/lib/function' @@ -342,16 +354,18 @@ const optional = pipe( Op.index(0), ) -const nestedMonocle: O.Option = +const nestedMonocle = optional.getOption({ a: { b: ['abc', 'def'] } }) +// nestedMonocle: O.Option ``` ```ts import { pipe } from 'fp-ts/function' import { get } from 'spectacles-ts' -const nestedSpectacles: O.Option = - pipe({ a : { b: ['abc', 'def'] } }, get('a', 'b', 0)) +const nestedSpectacles = + pipe({ a : { b: ['abc', 'def'] } }, get('a.b.[number]', 0)) +// nestedSpectacles: O.Option ``` You can see the simplicity that `spectacles-ts` offers @@ -364,10 +378,66 @@ monocle-ts has these advantages: - Can define an [isomorphism](https://github.com/gcanti/monocle-ts/blob/master/test/Iso.ts) between two objects - works with the [Map](https://github.com/gcanti/monocle-ts/blob/master/test/Ix.ts) type + +## Why use pipe for spectacles + +Let's see what libraries that don't use `pipe` look like + +```ts +import mapValues from 'lodash/mapValues' +import filter from 'lodash/filter' + +const data: Record = { a: 1, b: 2, c: 3 } + +const ugly = filter( + mapValues(data, (x) => x * 2), + (x) => x > 2 +) +// ugly = { b: 4, c: 6 } +``` + +This is a bit difficult to read. `mapValues` is nested inside `filter` - this could get messy if we add more functions. We can imagine that this might look much nicer if our data were an array - something like `data.map(x => ..).filter(x => ..)`. Is this possible with an object? + +```ts +import _ from 'lodash' + +const chained = _.chain(data) + .mapValues(x => x * 2) + .filter(x => x > 1) + .values() +// chained = { b: 4, c: 6 } +``` + +Much nicer! But this comes with a caveat - now we are importing all 600KB of lodash for two simple functions + +`pipe` gives us the best of both worlds: + +```ts +import { pipe } from 'fp-ts/function' +import { map, filter } from 'fp-ts/ReadonlyRecord' + +const piped = pipe( + data, + map(x => x * 2), + filter(x => x > 1) +) +// piped = { b: 4, c: 6 } +``` + +Legibility and economy - that's why we use `pipe` as much as possible + +Here's a more in-depth article about [how pipe-able functions work](https://paulgray.net/pipeable-apis/). [Here's one of the original articles](https://medium.com/bootstart/why-using-chain-is-a-mistake-9bc1f80d51ba) motivating their use + ## Conclusion I hope spectacles-ts can help you modify data both immutably & ergonomically! +Follow me on twitter! [@typesafeFE](https://twitter.com/typesafeFE) + +## Note + +An earlier version of spectacles used tuples for pathnames instead of string literals. This document has been updated to reflect the changes + **** CREDITS: diff --git a/package.json b/package.json index 1141a0f..001b3df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "spectacles-ts", - "version": "1.0.3", + "version": "1.0.4", "main": "./dist/index.js", "types": "./dist/index.d.ts", "sideEffects": false, diff --git a/readme-vid.gif b/readme-vid.gif index 8acbefd..0d984cd 100644 Binary files a/readme-vid.gif and b/readme-vid.gif differ diff --git a/src/util/Paths.ts b/src/util/Paths.ts index e0cd22b..3e49196 100644 --- a/src/util/Paths.ts +++ b/src/util/Paths.ts @@ -15,7 +15,7 @@ type _Paths = true extends type BubbleUp> = UnionToIntersection>>; type _BubbleUp> = { - [K in TupleKeyof]-?: Match< + [K in keyof A]-?: Match< A[K], { nullable: Record<`${Extract}?`, NonNullable>; @@ -25,7 +25,7 @@ type _BubbleUp> = { >}`]: A[K][K2]; }; tuple: { - [K2 in keyof A[K] as `${Extract}${Extract extends "" ? "" : "."}[${Extract< + [K2 in TupleKeyof as `${Extract}${Extract extends "" ? "" : "."}[${Extract< K2, string >}]`]: A[K][K2]; @@ -85,7 +85,13 @@ type Match< sum: unknown; other: unknown; } -> = Discriminant extends never +> = true extends IsNull + ? Matches["nullable"] + : [A] extends [Option] + ? Matches["option"] + : [A] extends [Either] + ? Matches["either"] + : Discriminant extends never ? [A] extends [readonly unknown[]] ? TupleKeyof extends never ? Matches["array"] @@ -94,13 +100,7 @@ type Match< ? Matches["record"] : true extends IsRecord ? Matches["struct"] - : true extends IsNull - ? Matches["nullable"] : Matches["other"] - : Option extends A - ? Matches["option"] - : Either extends A - ? Matches["either"] : Matches["sum"]; // Credit to Stefan Baumgartner