Skip to content

Commit

Permalink
WIP create fpSet
Browse files Browse the repository at this point in the history
  • Loading branch information
Siegrift committed Jan 19, 2020
1 parent 107801a commit af27875
Show file tree
Hide file tree
Showing 5 changed files with 301 additions and 22 deletions.
24 changes: 24 additions & 0 deletions src/common/baseSet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { isObject, shallowCopy } from '../common/utils'

const baseSet = (source: any, path: any, value: any) => {
const returnObject = shallowCopy(source, Number.isInteger(path[0]) ? [] : {})
let currentObject = returnObject
let index = 0
while (index < path.length) {
if (
!Array.isArray(currentObject[path[index]]) &&
!isObject(currentObject[path[index]])
) {
currentObject[path[index]] = Number.isInteger(path[index + 1]) ? [] : {}
}
if (index === path.length - 1) currentObject[path[index]] = value
else {
currentObject[path[index]] = shallowCopy(currentObject[path[index]])
}
currentObject = currentObject[path[index]]
index += 1
}
return returnObject
}

export default baseSet
127 changes: 127 additions & 0 deletions src/fpSet/fpSet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { Optional, UnwrapOptional as U } from '../common/types'
import baseSet from '../common/baseSet'

type FpSet1<T, K1 extends keyof T> = T extends any[]
? T
: Pick<T, Exclude<keyof T, K1>> &
{ [KK1 in K1]-?: Required<Pick<T, KK1>>[KK1] }

type FpSet2<T, K1 extends keyof T, K2 extends keyof U<T[K1]>> = T extends any[]
? T
: Pick<T, Exclude<keyof T, K1>> &
{ [KK1 in K1]-?: Required<{ [key in K1]: FpSet1<U<T[K1]>, K2> }>[KK1] }

type FpSet3<
T,
K1 extends keyof T,
K2 extends keyof U<T[K1]>,
K3 extends keyof U<U<T[K1]>[K2]>
> = T extends any[]
? T
: Pick<T, Exclude<keyof T, K1>> &
{
[KK1 in K1]-?: Required<{ [key in K1]: FpSet2<U<T[K1]>, K2, K3> }>[KK1]
}

type FpSet4<
T,
K1 extends keyof T,
K2 extends keyof U<T[K1]>,
K3 extends keyof U<U<T[K1]>[K2]>,
K4 extends keyof U<U<U<T[K1]>[K2]>[K3]>
> = T extends any[]
? T
: Pick<T, Exclude<keyof T, K1>> &
{
[KK1 in K1]-?: Required<
{ [key in K1]: FpSet3<U<T[K1]>, K2, K3, K4> }
>[KK1]
}

type FpSet5<
T,
K1 extends keyof T,
K2 extends keyof U<T[K1]>,
K3 extends keyof U<U<T[K1]>[K2]>,
K4 extends keyof U<U<U<T[K1]>[K2]>[K3]>,
K5 extends keyof U<U<U<U<T[K1]>[K2]>[K3]>[K4]>
> = T extends any[]
? T
: Pick<T, Exclude<keyof T, K1>> &
{
[KK1 in K1]-?: Required<
{ [key in K1]: FpSet4<U<T[K1]>, K2, K3, K4, K5> }
>[KK1]
}

// NOTE: TS doesn't allow partial genertic type argument inference
// see: https://github.com/microsoft/TypeScript/pull/26349
// see: https://medium.com/@nandiinbao/partial-type-argument-inference-in-typescript-and-workarounds-for-it-d7c772788b2e
interface FpSetFn {
<T>(): <K1 extends keyof T>(
path: [K1],
value: T[K1],
) => (source: Optional<T>) => FpSet1<T, K1>

<T>(): <K1 extends keyof T, K2 extends keyof U<T[K1]>>(
path: [K1, K2],
value: U<T[K1]>[K2],
) => (source: Optional<T>) => FpSet2<T, K1, K2>

<T>(): <
K1 extends keyof T,
K2 extends keyof U<T[K1]>,
K3 extends keyof U<U<T[K1]>[K2]>
>(
path: [K1, K2, K3],
value: U<U<T[K1]>[K2]>[K3],
) => (source: Optional<T>) => FpSet3<T, K1, K2, K3>

<T>(): <
K1 extends keyof T,
K2 extends keyof U<T[K1]>,
K3 extends keyof U<U<T[K1]>[K2]>,
K4 extends keyof U<U<U<T[K1]>[K2]>[K3]>
>(
path: [K1, K2, K3, K4],
value: U<U<U<T[K1]>[K2]>[K3]>[K4],
) => (source: Optional<T>) => FpSet4<T, K1, K2, K3, K4>

<T>(): <
K1 extends keyof T,
K2 extends keyof U<T[K1]>,
K3 extends keyof U<U<T[K1]>[K2]>,
K4 extends keyof U<U<U<T[K1]>[K2]>[K3]>,
K5 extends keyof U<U<U<U<T[K1]>[K2]>[K3]>[K4]>
>(
path: [K1, K2, K3, K4, K5],
value: U<U<U<U<T[K1]>[K2]>[K3]>[K4]>[K5],
) => (source: Optional<T>) => FpSet5<T, K1, K2, K3, K4, K5>
}

// NOTE: use private implementation because typedoc generates wrong documentation.
const fpSetImplementation: FpSetFn = () => (path: any[], value: any) => (
source: any,
) => baseSet(source, path, value)

/**
* Sets the value on the specified path in source value. If the path in the source doesn't exist it
* will be created. Note, that we don't know what is the type of the object at runtime. Due to this,
* if the path value is number, we create an array, otherwise object.
*
* Source value can be nullable or undefinable, and path is treated as if the source (and all
* intermediate) values are required (because nullable and undefinable types can't have keys).
*
* Path supports up to 5 elements. This means, you are not able to use this helper if you need more.
*
* Return type will be the same as the source type, where any optional values along the path are
* made required (because they are created).
*
* @param source source, in which the nested value should be set.
* @param path path array of the nested value in the source
* @param value value to be set in source on specified path
* @returns source value with value on path set
*/
export const fpSet = fpSetImplementation

export default fpSet
1 change: 1 addition & 0 deletions src/fpSet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './fpSet'
24 changes: 2 additions & 22 deletions src/set/set.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Optional, UnwrapOptional as U } from '../common/types'
import { isObject, shallowCopy } from '../common/utils'
import baseSet from '../common/baseSet'

type Set1<T, K1 extends keyof T> = T extends any[]
? T
Expand Down Expand Up @@ -98,27 +98,7 @@ interface SetFn {
}

// NOTE: use private implementation because typedoc generates wrong documentation.
const setImplementation: SetFn = (source: any, path: any[], value: any) => {
const returnObject = shallowCopy(source, Number.isInteger(path[0]) ? [] : {})
let currentObject = returnObject
let index = 0
while (index < path.length) {
if (
!Array.isArray(currentObject[path[index]]) &&
!isObject(currentObject[path[index]])
) {
currentObject[path[index]] = Number.isInteger(path[index + 1]) ? [] : {}
}
if (index === path.length - 1) currentObject[path[index]] = value
else {
currentObject[path[index]] = shallowCopy(currentObject[path[index]])
}
currentObject = currentObject[path[index]]
index += 1
}
return returnObject
}

const setImplementation: SetFn = baseSet
/**
* Sets the value on the specified path in source value. If the path in the source doesn't exist it
* will be created. Note, that we don't know what is the type of the object at runtime. Due to this,
Expand Down
147 changes: 147 additions & 0 deletions src/test/fpSet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import fpSet from '../fpSet'
import { State } from './common'
import { Dictionary } from '../common/types'

describe('fpSet', () => {
let state: State

beforeEach(() => {
state = {
users: [{ id: 56, key: 'key' }],
dict: {
someId: 'hello',
},
a: { b: { c: { d: { e: '123' } } } },
}
})

describe('set or override the nested value in object', () => {
test('in array', () => {
state = fpSet<State>()(['users', 0], { id: 777, key: 'new' })(state)

expect(state).toEqual({
users: [{ id: 777, key: 'new' }],
dict: {
someId: 'hello',
},
a: { b: { c: { d: { e: '123' } } } },
})
})

test('in dictionary', () => {
state = fpSet<State>()(['dict'], { newKey: 777 })(state)

expect(state).toEqual({
users: [{ id: 56, key: 'key' }],
dict: {
newKey: 777,
},
a: { b: { c: { d: { e: '123' } } } },
})
})

test('set up to 5 levels', () => {
const obj = { a: state.a }
const expected = { a: { b: { c: { d: { e: '123' } } } } }
let setObj: Pick<State, 'a'> = obj

setObj = fpSet<typeof obj>()(['a'], { b: { c: { d: { e: '123' } } } })(
obj,
)
expect(setObj).toEqual(expected)

setObj = fpSet<typeof obj>()(['a', 'b'], { c: { d: { e: '123' } } })(obj)
expect(setObj).toEqual(expected)

setObj = fpSet<typeof obj>()(['a', 'b', 'c'], { d: { e: '123' } })(obj)
expect(setObj).toEqual(expected)

setObj = fpSet<typeof obj>()(['a', 'b', 'c', 'd'], { e: '123' })(obj)
expect(setObj).toEqual(expected)

setObj = fpSet<typeof obj>()(['a', 'b', 'c', 'd', 'e'], '123')(obj)
expect(setObj).toEqual(expected)
})
})

test('is immutable', () => {
fpSet(state, ['a', 'b', 'c', 'd', 'e'], '123')

expect(state).toBe(state)
})

describe("if path doesn't exist, it is created in the value", () => {
test('value is primitive', () => {
expect(fpSet(null as any, ['key'], '')).toEqual({ key: '' })
expect(fpSet(undefined as any, ['key'], '')).toEqual({ key: '' })
expect(fpSet('' as any, ['key'], '')).toEqual({ key: '' })
expect(fpSet(123 as any, ['key'], '')).toEqual({ key: '' })
})

test('create new index in array', () => {
state = fpSet(state, ['users', 3, 'id'], 777)

expect(state).toEqual({
users: [{ id: 56, key: 'key' }, undefined, undefined, { id: 777 }],
dict: {
someId: 'hello',
},
a: { b: { c: { d: { e: '123' } } } },
})
})

test('create new property in dictionary', () => {
state = fpSet(state, ['dict', 'newKey'], 777)

expect(state).toEqual({
users: [{ id: 56, key: 'key' }],
dict: {
someId: 'hello',
newKey: 777,
},
a: { b: { c: { d: { e: '123' } } } },
})
})

describe('if path is number an array is created, otherwise object is created', () => {
test('correct root value', () => {
expect(fpSet(null as any, ['hello'], 'str')).toEqual({ hello: 'str' })
expect(fpSet(null as any, [1], 'str')).toEqual([undefined, 'str'])
})

test('deep path', () => {
type A = string[]
type D = { [key: string]: A }
type T = { req: { opt?: D | null } }
let obj: T = { req: { opt: null } }

obj = fpSet<typeof obj>()(['req', 'opt', 'key', 1], 'str')(obj)

expect(obj).toEqual({
req: { opt: { key: [undefined, 'str'] } },
})
})
})

test('works with objects with other properties', () => {
type A = { a: { b: boolean; c: Dictionary<string> }; d: string }
const obj: A = { a: { b: true, c: {} }, d: 'str' }

const newObj: A = fpSet<typeof obj>()(['a', 'b'], false)(obj)
expect(newObj).toEqual({ a: { b: false, c: {} }, d: 'str' })
})

test('works with union of properties', () => {
interface A {
a: 'fixed'
b: string
c: boolean
}
const obj: A = { a: 'fixed', b: 'str', c: true }
const prop = 'a' as 'a' | 'b'

const s: A = fpSet<typeof obj>()([prop], 'fixed')(obj)
expect(s).toEqual(obj)
})
})
})

0 comments on commit af27875

Please sign in to comment.