Skip to content

Instantly share code, notes, and snippets.

@brombal
Last active December 17, 2021 17:33
Show Gist options
  • Save brombal/c6c9ccdd9ed5a7bd1003ca791273da2f to your computer and use it in GitHub Desktop.
Save brombal/c6c9ccdd9ed5a7bd1003ca791273da2f to your computer and use it in GitHub Desktop.
/*
# mapObject
mapObject<TOut, TIn = any>(
object: TIn,
callback: (value: any, path: string[], root: any) => any,
options?: {
mutate?: boolean;
thisArg?: any;
omitEmpty?: boolean;
},
): TOut;
Maps an object recursively by calling `callback` on each property and replacing the value with the result.
`callback` is not invoked for properties that contain objects or arrays, but instead `mapObject` is called
recursively on those values.
`callback` is passed the following parameters:
- `value` - The current value
- `path` - The path to the value as an array
- `object` - The original object being mapped
If the result of `callback` is undefined, the property will be absent from the resulting object.
`options` is an object with the following properties:
- `mutate` - If true, the original object will be mutated (default false)
- `omitEmpty` - If true, mapped objects and arrays that are empty will be omitted from the result (default false)
- `thisArg` - Sets the `this` value for `callback`
*/
const isObject = (value: any) => value?.constructor.prototype === Object.prototype;
interface MapObjectFn {
<TIn>(
object: TIn,
callback: (value: any, path: string[], root: any) => any,
options?: MapObjectOptions,
): TIn;
<TOut, TIn = any>(
object: TIn,
callback: (value: any, path: string[], root: any) => any,
options?: MapObjectOptions,
): TOut;
}
interface MapObjectOptions {
mutate?: boolean;
thisArg?: any;
omitEmpty?: boolean;
}
export const mapObject: MapObjectFn = function mapObject<TOut, TIn>(
object: TIn,
callback: (value: any, path: string[], root: any) => any,
options?: MapObjectOptions,
): TOut {
return _mapObject(object, callback, options || {}, new WeakMap(), []);
};
function _mapObject(
object: any,
callback: (value: any, path: string[], root: any) => any,
options: MapObjectOptions,
cache: any,
path: string[],
): any {
if (!Array.isArray(object) && !isObject(object)) return object;
if (cache.has(object)) return cache.get(object);
const result = options.mutate ? object : Array.isArray(object) ? [] : {};
cache.set(object, result);
for (const k in object) {
if (!Object.prototype.hasOwnProperty.call(object, k)) continue;
const val = object[k];
if (isObject(val) || Array.isArray(val)) {
const mapped = _mapObject(val, callback, options, cache, [...path, k]);
if (
!options.omitEmpty ||
(isObject(mapped) && Object.keys(mapped).length) ||
(Array.isArray(mapped) && mapped.length)
)
result[k] = mapped;
else delete result[k];
} else {
const mapped = callback.call(options.thisArg, val, [...path, k], object);
if (mapped !== undefined) result[k] = mapped;
}
}
return result;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment