Skip to content

Commit

Permalink
feat: Allow the updating of an union property by using a type guard (#12
Browse files Browse the repository at this point in the history
)
  • Loading branch information
AlexGalays committed Nov 25, 2017
1 parent 4165e70 commit feea68b
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 31 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ This library only does simple updates (e.g setting, modifying or deleting a valu
* [Update an Array item](#update-array-item)
* [Update a nested property using its current value](#update-nested-property-modify)
* [Update a nested property on a nullable path](#update-nested-nullable-property)
* [Update a nested union property](#update-nested-union-property)
* [Reuse a nested updater](#reuse-nested-updater)


Expand Down Expand Up @@ -246,6 +247,26 @@ deepUpdate<Person>({})
.set('en')
```

<a name="update-nested-union-property"></a>
## Update a nested union property

```ts
import { deepUpdate } from 'immupdate'

type A = { type: 'a', data: string }
type B = { type: 'b', data: number }
type Container = { aOrB: A | B }
const isA = (u: A | B): u is A => u.type === 'a'

const container = { aOrB: { type: 'a', data: 'aa' } }

deepUpdate(container)
.at('aOrB')
.abortIfNot(isA)
.at('data')
.set('bb')
```

<a name="reuse-nested-updater"></a>
## Reuse a nested updater

Expand Down
83 changes: 53 additions & 30 deletions immupdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,81 +36,101 @@ export const DELETE = {} as any as undefined
export type ObjectLiteral = object & { reduceRight?: 'nope' }


export interface AtUpdater<R, O> {
__T: [R, O] // Strengthen structural typing
export interface AtUpdater<TARGET, CURRENT> {
__T: [TARGET, CURRENT] // Strengthen structural typing

/**
* Selects this Object key for update or further at() chaining
*/
at<K extends keyof O>(this: AtUpdater<R, ObjectLiteral>, key: K): Updater<R, O[K]>
at<K extends keyof CURRENT>(this: AtUpdater<TARGET, ObjectLiteral>, key: K): Updater<TARGET, CURRENT[K]>

/**
* Selects an Array index for update or further at() chaining
*/
at<A>(this: AtUpdater<R, A[]>, index: number): Updater<R, A | undefined>
at<A>(this: AtUpdater<TARGET, A[]>, index: number): Updater<TARGET, A | undefined>
}

// The at interface carrying a pre-bound value
export interface BoundAtUpdater<R, O> {
__T: [R, O]
export interface BoundAtUpdater<TARGET, CURRENT> {
__T: [TARGET, CURRENT]

/**
* Selects this Object key for update or further at() chaining
*/
at<K extends keyof O>(this: BoundAtUpdater<R, ObjectLiteral>, key: K): BoundUpdater<R, O[K]>
at<K extends keyof CURRENT>(this: BoundAtUpdater<TARGET, ObjectLiteral>, key: K): BoundUpdater<TARGET, CURRENT[K]>

/**
* Selects an Array index for update or further at() chaining
*/
at<A>(this: BoundAtUpdater<R, A[]>, index: number): BoundUpdater<R, A | undefined>
at<A>(this: BoundAtUpdater<TARGET, A[]>, index: number): BoundUpdater<TARGET, A | undefined>
}

export interface Updater<R, O> extends AtUpdater<R, O> {
__T: [R, O]
export interface Updater<TARGET, CURRENT> extends AtUpdater<TARGET, CURRENT> {
__T: [TARGET, CURRENT]

/**
* Sets the value at the currently selected path.
*/
set(value: O): (target: R) => R
set(value: CURRENT): (target: TARGET) => TARGET

/**
* Modifies the value at the specified path. The current value is passed.
*/
modify(modifier: (value: O) => O): (target: R) => R
modify(modifier: (value: CURRENT) => CURRENT): (target: TARGET) => TARGET

/**
* Makes the previous nullable chain level 'safe' by using a default value
*/
withDefault<B, C extends B>(this: Updater<R, B | undefined>, defaultValue: C): Updater<R, B>
withDefault<B, C extends B>(this: Updater<TARGET, B | undefined>, defaultValue: C): Updater<TARGET, B>

/**
* Aborts the whole update operation if the previous chain level is null or undefined.
*/
abortIfUndef<B>(this: Updater<R, B | undefined>): Updater<R, B>
abortIfUndef<B>(this: Updater<TARGET, B | undefined>): Updater<TARGET, B>

/**
* Aborts the whole update operation if the previous chain level doesn't verify a type guard
*/
abortIfNot<C extends CURRENT>(predicate: (value: CURRENT) => value is C): Updater<TARGET, C>

/**
* Aborts the whole update operation if the previous chain level doesn't verify a predicate
*/
abortIfNot(predicate: (value: CURRENT) => boolean): Updater<TARGET, CURRENT>
}

export interface BoundUpdater<R, O> extends BoundAtUpdater<R, O> {
__T: [R, O]
export interface BoundUpdater<TARGET, CURRENT> extends BoundAtUpdater<TARGET, CURRENT> {
__T: [TARGET, CURRENT]

/**
* Sets the value at the currently selected path.
*/
set(value: O): R
set(value: CURRENT): TARGET

/**
* Modifies the value at the specified path. The current value is passed.
*/
modify(modifier: (value: O) => O): R
modify(modifier: (value: CURRENT) => CURRENT): TARGET

/**
* Makes the previous nullable chain level 'safe' by using a default value
*/
withDefault<B, C extends B>(this: BoundUpdater<R, B | undefined>, defaultValue: C): BoundUpdater<R, B>
withDefault<B, C extends B>(this: BoundUpdater<TARGET, B | undefined>, defaultValue: C): BoundUpdater<TARGET, B>

/**
* Aborts the whole update operation if the previous chain level is null or undefined.
*/
abortIfUndef<B>(this: BoundUpdater<R, B | undefined>): BoundUpdater<R, B>
abortIfUndef<B>(this: BoundUpdater<TARGET, B | undefined>): BoundUpdater<TARGET, B>

/**
* Aborts the whole update operation if the previous chain level doesn't verify a type guard
*/
abortIfNot<C extends CURRENT>(predicate: (value: CURRENT) => value is C): BoundUpdater<TARGET, C>

/**
* Aborts the whole update operation if the previous chain level doesn't verify a predicate
*/
abortIfNot(predicate: (value: CURRENT) => boolean): BoundUpdater<TARGET, CURRENT>
}


Expand All @@ -131,12 +151,13 @@ interface WithDefault {
parent: any
}

interface AbortIfUndef {
type: 'abortIfUndef'
interface AbortIfNot {
type: 'abortIfNot'
predicate: any
parent: any
}

type UpdaterData = Root | At | WithDefault | AbortIfUndef
type UpdaterData = Root | At | WithDefault | AbortIfNot



Expand Down Expand Up @@ -191,8 +212,12 @@ class _Updater {
return new _Updater({ type: 'withDefault', parent: this, defaultValue: value })
}

abortIfNot(predicate: any): any {
return new _Updater({ type: 'abortIfNot', parent: this, predicate })
}

abortIfUndef(): any {
return new _Updater({ type: 'abortIfUndef', parent: this })
return this.abortIfNot((value: any) => value !== undefined)
}

findBoundTarget() {
Expand Down Expand Up @@ -227,13 +252,11 @@ class _Updater {
return { host: newHost, field: newField }
}

const value = previousHost[field]

if (this.data.type === 'abortIfUndef' && value === undefined) {
if (this.data.type === 'abortIfNot' && this.data.predicate(host) === false) {
return { host, field, aborted: true }
}

if (this.data.type === 'withDefault' && value === undefined) {
if (this.data.type === 'withDefault' && previousHost[field] === undefined) {
const nextValue = this.data.defaultValue
const newHost = isLast ? previousHost : nextValue
previousHost[field] = nextValue
Expand Down Expand Up @@ -290,8 +313,8 @@ function clone(obj: any): any {
return cloned
}

export function deepUpdate<O extends object>(target: O): BoundAtUpdater<O, O>
export function deepUpdate<O extends object>(): AtUpdater<O, O>
export function deepUpdate<TARGET extends object>(target: TARGET): BoundAtUpdater<TARGET, TARGET>
export function deepUpdate<TARGET extends object>(): AtUpdater<TARGET, TARGET>
export function deepUpdate(target?: any): any {
return new _Updater({ type: 'root', boundTarget: target })
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "immupdate",
"version": "1.1.6",
"version": "1.1.7",
"description": "Immutable update for Objects and Arrays",
"license": "MIT",

Expand Down
70 changes: 70 additions & 0 deletions test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,76 @@ describe('immupdate', () => {
expect(updated4).toNotBe(obj2)
})

it('can update an union member with an instance of this union', () => {

type A = { type: 'a', data: number }
type B = { type: 'b', data: string }
type U = A | B

type Obj = { u: U }

const obj: Obj = { u: { type: 'a', data: 10 } }

const updated = deepUpdate(obj)
.at('u')
.set({ type: 'b', data: '11' })

expect(updated).toEqual({
u: { type: 'b', data: '11' }
})
})

it('can abort if a simple condition is not met', () => {
type Obj = { a?: { version: number, data: string } }

const obj: Obj = { a: { version: 0, data: '001' } }

const updated = deepUpdate(obj)
.at('a')
.abortIfUndef()
.abortIfNot(a => a.version === 0)
.at('data')
.set('002')

expect(updated).toNotBe(obj)
expect(updated).toEqual({
a: { version: 0, data: '002' }
})

const obj2: Obj = { a: { version: 1, data: '001' } }

const updated2 = deepUpdate(obj2)
.at('a')
.abortIfUndef()
.abortIfNot(a => a.version === 0)
.set({ version: 2, data: 'zzz' })

expect(updated2).toBe(obj2)
})

it('can abort if a type guard is not passed', () => {

type A = { type: 'a', data: number }
type B = { type: 'b', data: string }
type U = A | B

const isA = (u: U): u is A => u.type === 'a'

type Obj = { u: U }

const obj: Obj = { u: { type: 'a', data: 10 } }

const updated = deepUpdate(obj)
.at('u')
.abortIfNot(isA)
.at('data')
.set(20)

expect(updated).toEqual({
u: { type: 'a', data: 20 }
})
})

})

})

0 comments on commit feea68b

Please sign in to comment.