-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
301 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from './fpSet' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
}) | ||
}) |