Updated answer
Here we extract the fields we care about into arrays, turn that into a multidimensional array (number of records by number of elements in an array), transpose that array, and then zip it back together as an object.
We use the same list of field names for the extraction and for reconstituting an object. It looks like this:
const inflate = (names) => (obj) =>
map (zipObj (names)) (transpose (props (names) (obj)))
const obj = {paths: ['path1', 'path2'], thumbnails: ['thumb1', 'thumb2'], sizes: [[100, 200], [120, 220]]}
console .log (inflate (['paths', 'thumbnails', 'sizes']) (obj))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js"></script>
<script> const {map, zipObj, transpose, props, paths} = R </script>
For your extended version, just run a subsequent map over the results:
const inflate = (names) => (obj) =>
map (zipObj (names)) (transpose (props (names) (obj)))
.map (({sizes: [width, height], ...rest}) => ({...rest, width, height}))
A specification-based approach
Ori Drori's answer challenged me to find a specification-based version that lets us declare that paths becomes path and the first element of sizes becomes width. Here is one rather nasty version of that idea. I'm out of time to clean it up, but you can see at least the skeleton of an interesting idea in this:
const regroup = (spec, names = uniq (values (spec) .map (x => x.split ('.') [0]))) => (obj) =>
map (applySpec (map (pipe (split ('.'), path)) (spec))) (map (zipObj (names)) (transpose (props (names) (obj))))
const obj = {paths: ['path1', 'path2'], thumbnails: ['thumb1', 'thumb2'], sizes: [[100, 200], [120, 220]]}
const spec = {path: 'paths', thumbnail: 'thumbnails', width: 'sizes.0', height: 'sizes.1'}
console .log (regroup (spec) (obj))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js"></script>
<script> const {uniq, values, map, applySpec, pipe, split, path, zipObj, transpose, props} = R </script>
Original answer
(I originally misread and thought you wanted something like the Cartesian product of the elements, not the matched indices one. I'm leaving it here as it does answer what to my mind is a more interesting question.)
TLDR
Below we discuss several implementations. A configurable one written in Ramda looks like this:
const inflate = pipe (map (unwind), pipeWith (chain))
// ...
inflate (['paths', 'thumbnails', 'sizes', 'sizes']) (obj)
A powerful, but less flexible vanilla one looks like this:
const inflate = (obj, _, __, field = Object .entries (obj) .find (([k, v]) => Array .isArray (v))) =>
field ? field [1] .flatMap ((v) => inflate ({...obj, [field [0]]: v})) : obj
// ...
inflate (obj)
Ramda implementation
Ramda has a function dedicated to this: unwind. It takes a property name and returns an array of values, one for each element of the array at that name. We can then simply pipe these together with chain, like this:
const inflate = pipeWith (chain) ([
unwind ('paths'),
unwind ('thumbnails'),
unwind ('sizes'),
unwind ('sizes')
])
const obj = {paths: ['path1', 'path2'], thumbnails: ['thumb1', 'thumb2'], sizes: [[100, 200], [120, 220]]}
console .log (inflate (obj))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js"></script>
<script> const {pipeWith, chain, unwind} = R </script>
If pipeWith is unfamiliar, this is equivalent to
const inflate = pipe (
unwind ('paths'),
chain (unwind ('thumbnails')),
chain (unwind ('sizes')),
chain (unwind ('sizes'))
)
where the chain calls are acting here similar to Array.prototype.flatMap.
More abstract Ramda implementation
But this calls out for a further abstraction. We can make that more declarative by extracting the list of paths we want to expand, so that we can simply call inflate (['paths', 'thumbnails', 'sizes', 'sizes']) (obj). This is an implementation:
const inflate = pipe (map (unwind), pipeWith (chain))
const obj = {paths: ['path1', 'path2'], thumbnails: ['thumb1', 'thumb2'], sizes: [[100, 200], [120, 220]]}
console .log (inflate (['paths', 'thumbnails', 'sizes', 'sizes']) (obj))
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js"></script>
<script> const {pipe, map, pipeWith, chain, unwind} = R </script>
Powerful, but less flexible vanilla implementation
But perhaps we don't want to specify the fields. If we just want to supply the object and expand all arrays, including nested ones, then we probably want to write a recursive version. At this point, Ramda would probably be a distraction. We can write a simple vanilla JS implementation like this:
const inflate = (obj, _, __, field = Object .entries (obj) .find (([k, v]) => Array .isArray (v))) =>
field ? field [1] .flatMap ((v) => inflate ({...obj, [field [0]]: v})) : obj
const obj = {paths: ['path1', 'path2'], thumbnails: ['thumb1', 'thumb2'], sizes: [[100, 200], [120, 220]]}
console .log (inflate (obj))
.as-console-wrapper {max-height: 100% !important; top: 0}
This is powerful. It works with any array properties, even if they're multidimensional. But it is inflexible. You cannot choose not to expand certain array properties. Whether this is good or bad will depend upon your circumstances.
We could use Ramda in the same manner. Here's a partial conversion to using Ramda functions:
const inflate = (obj) => call (
(field = pipe (toPairs, find (pipe (last, is(Array)))) (obj), [k, v] = field || []) =>
field ? chain ((v) => inflate (assoc (k, v, obj))) (field [1]) : obj
)
And we could continue to pull things out until we were totally point-free, but I think we'd slowly be losing readability here, unlike with the earlier Ramda versions, where we ended up with fairly elegant, readable implementations.
Names
None of these versions (in the original or updated answers) does name changes. We don't try to convert "sizes" to "size" or "paths" to "path". If you have control over the input format, I would suggest that you simply switch it there. If not, I would probably do that as a final step. You can do that using Ramda's applySpec or perhaps using something like renameKeys from Ramda's Cookbook. While it might be possible to fold this key renaming into the functions above, I would expect it to be less robust than simply doing a second pass.