// TypeScript Version 3.1.6 import * as invariant from 'invariant'; const hasOwnProperty = Object.prototype.hasOwnProperty; const splice = Array.prototype.splice; const toString = Object.prototype.toString; const type = obj => toString.call(obj) .slice(8, -1); const assign = Object.assign || /* istanbul ignore next */ ((target, source) => { getAllKeys(source) .forEach(key => { if (hasOwnProperty.call(source, key)) { target[key] = source[key]; } }); return target; }); const getAllKeys = typeof Object.getOwnPropertySymbols === 'function' ? obj => Object.keys(obj) .concat(Object.getOwnPropertySymbols(obj) as any) : /* istanbul ignore next */ obj => Object.keys(obj); /* istanbul ignore next */ function copy(object) { if (Array.isArray(object)) { return assign(object.constructor(object.length), object); } else if (type(object) === 'Map') { return new Map(object); } else if (type(object) === 'Set') { return new Set(object); } else if (object && typeof object === 'object') { const prototype = Object.getPrototypeOf(object); return assign(Object.create(prototype), object); } else { return object; } } export function newContext() { const commands = assign({}, defaultCommands); update.extend = (directive: string, fn: (param: any, old: T) => T) => { commands[directive] = fn; }; update.isEquals = (a, b) => a === b; return update; function update = never>( object: T, $spec: Spec, ): T { const spec = (typeof $spec === 'function') ? { $apply: $spec } : $spec; if (!(Array.isArray(object) && Array.isArray(spec))) { invariant( !Array.isArray(spec), 'update(): You provided an invalid spec to update(). The spec may ' + 'not contain an array except as the value of $set, $push, $unshift, ' + '$splice or any custom command allowing an array value.', ); } invariant( typeof spec === 'object' && spec !== null, 'update(): You provided an invalid spec to update(). The spec and ' + 'every included key path must be plain objects containing one of the ' + 'following commands: %s.', Object.keys(commands) .join(', '), ); let nextObject = object; getAllKeys(spec) .forEach(key => { if (hasOwnProperty.call(commands, key)) { const objectWasNextObject = object === nextObject; nextObject = commands[key](spec[key], nextObject, spec, object); if (objectWasNextObject && update.isEquals(nextObject, object)) { nextObject = object; } } else { const nextValueForKey = type(object) === 'Map' ? update((object as any as Map).get(key), spec[key]) : update(object[key], spec[key]); const nextObjectValue = type(nextObject) === 'Map' ? (nextObject as any as Map).get(key) : nextObject[key]; if (!update.isEquals(nextValueForKey, nextObjectValue) || typeof nextValueForKey === 'undefined' && !hasOwnProperty.call(object, key) ) { if (nextObject === object) { nextObject = copy(object); } if (type(nextObject) === 'Map') { (nextObject as any as Map).set(key, nextValueForKey); } else { nextObject[key] = nextValueForKey; } } } }); return nextObject; } } const defaultCommands = { $push: (value, nextObject, spec) => { invariantPushAndUnshift(nextObject, spec, '$push'); return value.length ? nextObject.concat(value) : nextObject; }, $unshift: (value, nextObject, spec) => { invariantPushAndUnshift(nextObject, spec, '$unshift'); return value.length ? value.concat(nextObject) : nextObject; }, $splice: (value, nextObject, spec, originalObject) => { invariantSplices(nextObject, spec); value.forEach(args => { invariantSplice(args); if (nextObject === originalObject && args.length) { nextObject = copy(originalObject); } splice.apply(nextObject, args); }); return nextObject; }, $set: (value, _nextObject, spec) => { invariantSet(spec); return value; }, $toggle: (targets, nextObject) => { invariantSpecArray(targets, '$toggle'); const nextObjectCopy = targets.length ? copy(nextObject) : nextObject; targets.forEach(target => { nextObjectCopy[target] = !nextObject[target]; }); return nextObjectCopy; }, $unset: (value, nextObject, _spec, originalObject) => { invariantSpecArray(value, '$unset'); value.forEach(key => { if (Object.hasOwnProperty.call(nextObject, key)) { if (nextObject === originalObject) { nextObject = copy(originalObject); } // tslint:disable-next-line:no-dynamic-delete delete nextObject[key]; } }); return nextObject; }, $add: (values, nextObject, _spec, originalObject) => { invariantMapOrSet(nextObject, '$add'); invariantSpecArray(values, '$add'); if (type(nextObject) === 'Map') { values.forEach(pair => { const key = pair[0]; const value = pair[1]; /* istanbul ignore next */ if (nextObject === originalObject && nextObject.get(key) !== value) { nextObject = copy(originalObject); } nextObject.set(key, value); }); } else { values.forEach(value => { if (nextObject === originalObject && !nextObject.has(value)) { nextObject = copy(originalObject); } nextObject.add(value); }); } return nextObject; }, $remove: (value, nextObject, _spec, originalObject) => { invariantMapOrSet(nextObject, '$remove'); invariantSpecArray(value, '$remove'); value.forEach(key => { if (nextObject === originalObject && nextObject.has(key)) { nextObject = copy(originalObject); } nextObject.delete(key); }); return nextObject; }, $merge: (value, nextObject, _spec, originalObject) => { invariantMerge(nextObject, value); getAllKeys(value) .forEach(key => { if (value[key] !== nextObject[key]) { if (nextObject === originalObject) { nextObject = copy(originalObject); } nextObject[key] = value[key]; } }); return nextObject; }, $apply: (value, original) => { invariantApply(value); return value(original); }, }; const defaultExport = newContext(); (defaultExport as any).default = defaultExport; (defaultExport as any).newContext = newContext export = defaultExport; // invariants function invariantPushAndUnshift(value, spec, command) { invariant( Array.isArray(value), 'update(): expected target of %s to be an array; got %s.', command, value, ); invariantSpecArray(spec[command], command); } function invariantSpecArray(spec, command) { invariant( Array.isArray(spec), 'update(): expected spec of %s to be an array; got %s. ' + 'Did you forget to wrap your parameter in an array?', command, spec, ); } function invariantSplices(value, spec) { invariant( Array.isArray(value), 'Expected $splice target to be an array; got %s', value, ); invariantSplice(spec.$splice); } function invariantSplice(value) { invariant( Array.isArray(value), 'update(): expected spec of $splice to be an array of arrays; got %s. ' + 'Did you forget to wrap your parameters in an array?', value, ); } function invariantApply(fn) { invariant( typeof fn === 'function', 'update(): expected spec of $apply to be a function; got %s.', fn, ); } function invariantSet(spec) { invariant( Object.keys(spec).length === 1, 'Cannot have more than one key in an object with $set', ); } function invariantMerge(target, specValue) { invariant( specValue && typeof specValue === 'object', 'update(): $merge expects a spec of type \'object\'; got %s', specValue, ); invariant( target && typeof target === 'object', 'update(): $merge expects a target of type \'object\'; got %s', target, ); } function invariantMapOrSet(target, command) { const typeOfTarget = type(target); invariant( typeOfTarget === 'Map' || typeOfTarget === 'Set', 'update(): %s expects a target of type Set or Map; got %s', command, typeOfTarget, ); } // Usage with custom commands is as follows: // // interface MyCommands { // $foo: string; // } // // update>(..., { $foo: "bar" }); // // It is suggested that if you use custom commands frequently, you wrap and re-export a // properly-typed version of `update`: // // function myUpdate(object: T, spec: Spec>) { // return update(object, spec); // } // // See https://github.com/kolodny/immutability-helper/pull/108 for explanation of why this // type exists. export type CustomCommands = T & { __noInferenceCustomCommandsBrand: any }; type Spec = never> = | ( T extends (Array | ReadonlyArray) ? ArraySpec : T extends (Map | ReadonlyMap) ? MapSpec : T extends (Set | ReadonlySet) ? SetSpec : T extends object ? ObjectSpec : never ) | { $set: T } | { $apply: (v: T) => T } | ((v: T) => T) | (C extends CustomCommands ? O : never); type ArraySpec> = | { $push: T[] } | { $unshift: T[] } | { $splice: Array<[number, number?] | [number, number, ...T[]]> } | { [index: string]: Spec }; // Note that this does not type check properly if index: number. type MapSpec = | { $add: Array<[K, V]> } | { $remove: K[] }; type SetSpec = | { $add: T[] } | { $remove: T[] }; type ObjectSpec> = | { $toggle: Array } | { $unset: Array } | { $merge: Partial } | { [K in keyof T]?: Spec };