diff --git a/src/apply.ts b/src/apply.ts index 06dc539..17b3761 100644 --- a/src/apply.ts +++ b/src/apply.ts @@ -1,4 +1,11 @@ -import { Draft, Options, Patches, DraftType, Operation } from './interface'; +import { Operation, DraftType } from './interface'; +import type { + Draft, + Patches, + ApplyMutableOptions, + ApplyOptions, + ApplyResult, +} from './interface'; import { deepClone, get, getType, isDraft, unescapePath } from './utils'; import { create } from './create'; @@ -23,14 +30,11 @@ import { create } from './create'; * expect(state).toEqual(apply(baseState, patches)); * ``` */ -export function apply( - state: T, - patches: Patches, - applyOptions?: Pick< - Options, - Exclude, 'enablePatches'> - > -) { +export function apply< + T extends object, + F extends boolean = false, + A extends ApplyOptions = ApplyOptions +>(state: T, patches: Patches, applyOptions?: A): ApplyResult { let i: number; for (i = patches.length - 1; i >= 0; i -= 1) { const { value, op, path } = patches[i]; @@ -45,7 +49,7 @@ export function apply( if (i > -1) { patches = patches.slice(i + 1); } - const mutate = (draft: Draft) => { + const mutate = (draft: Draft | T) => { patches.forEach((patch) => { const { path: _path, op } = patch; const path = unescapePath(_path); @@ -122,15 +126,19 @@ export function apply( } }); }; + if ((applyOptions as ApplyMutableOptions)?.mutable) { + mutate(state); + return undefined as ApplyResult; + } if (isDraft(state)) { if (applyOptions !== undefined) { throw new Error(`Cannot apply patches with options to a draft.`); } mutate(state as Draft); - return state; + return state as ApplyResult; } return create(state, mutate, { ...applyOptions, enablePatches: false, - }); + }) as any; } diff --git a/src/interface.ts b/src/interface.ts index 0fbabe3..58b83ad 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -102,6 +102,13 @@ export type Mark = ( ? BaseMark : MarkWithCopy; +export interface ApplyMutableOptions { + /** + * If it's `true`, the state will be mutated directly. + */ + mutable?: boolean; +} + export interface Options { /** * In strict mode, Forbid accessing non-draftable values and forbid returning a non-draft value. @@ -190,3 +197,16 @@ export type Draft = T extends Primitive | AtomicObject : T extends object ? DraftedObject : T; + +export type ApplyOptions = + | Pick< + Options, + Exclude, 'enablePatches'> + > + | ApplyMutableOptions; + +export type ApplyResult< + T extends object, + F extends boolean = false, + A extends ApplyOptions = ApplyOptions +> = A extends { mutable: true } ? void : T; diff --git a/test/apply.test.ts b/test/apply.test.ts index b520751..3d16aad 100644 --- a/test/apply.test.ts +++ b/test/apply.test.ts @@ -1305,16 +1305,6 @@ test('merge multiple patches', () => { enablePatches: true, } ); - console.log( - JSON.stringify( - { - patches, - inversePatches, - }, - null, - 2 - ) - ); const [state1, patches1, inversePatches1] = create( state, (draft) => { @@ -1357,3 +1347,46 @@ test('merge multiple patches', () => { ]); expect(errorPrevState).not.toEqual(arr); }); + +test('base - mutate', () => { + const baseState = { + a: { + c: 1, + }, + }; + const [state, patches, inversePatches] = create( + baseState, + (draft) => { + draft.a.c = 2; + }, + { + enablePatches: true, + } + ); + expect(state).toEqual({ a: { c: 2 } }); + expect({ patches, inversePatches }).toEqual({ + patches: [ + { + op: 'replace', + path: ['a', 'c'], + value: 2, + }, + ], + inversePatches: [ + { + op: 'replace', + path: ['a', 'c'], + value: 1, + }, + ], + }); + const nextState = apply(baseState, patches); + expect(nextState).toEqual({ a: { c: 2 } }); + expect(baseState).toEqual({ a: { c: 1 } }); + + const result = apply(baseState, patches, { + mutable: true, + }); + expect(baseState).toEqual({ a: { c: 2 } }); + expect(result).toBeUndefined(); +});