DEV Community

Vladimir Klepov
Vladimir Klepov

Posted on • Originally published at blog.thoughtspile.tech on

Cleaner ways to build dynamic JS arrays

Building dynamic arrays in JS is often messy. It goes like this: you have a default array, and you need some items to appear based on a condition. So you add an if (condition) array.push(item). Then you need to shuffle things around and bring in an unshift or two, and maybe even a splice. Soon, your array building code is a crazy mess of ifs with no way to tell what can be in the final array, and in which order. Something like this (yes, I’m building a CLI lint runner):

let args = ['--ext', '.ts,.tsx,.js,.jsx'];
if (cache) {
  args.push(
    '--cache',
    '--cache-location', path.join(\_\_dirname, '.cache'));
}
if (source != null) {
  args.unshift(source);
}
if (isTeamcity) {
  args = args.concat(['--format', 'node\_modules/eslint-teamcity']);
}
Enter fullscreen mode Exit fullscreen mode

Luckily, I’m here to end the struggle with three great ways to clean up this mess! As a bonus, I’ll show you how to apply these techniques to strings as well!

Chained concat

The first trick is to replace every if block with a .concat(cond ? [...data] : []). Luckily, concat is chainable, and working with it is a joy:

const args = [
  '--ext', '.ts,.tsx,.js,.jsx',
].concat(cache ? [
  '--cache',
  '--cache-location', path.join(\_\_dirname, '.cache')
] : []).concat(isTeamcity ? [
  '--format', 'node\_modules/eslint-teamcity'
] : []);
Enter fullscreen mode Exit fullscreen mode

Much better! The array is consistently formatted and easier to read, with clear conditional blocks. If you’re paying attention, you’ll notice I missed the unshift bit — that’s because at the beginning, you don’t have an array to .concat() to. Why don’t we just create it?

const args = [].concat(source !== null ? [
  source
] : []).concat([
  '--ext', '.ts,.tsx,.js,.jsx',
]).concat(cache ? [
  '--cache',
  '--cache-location', path.join(\_\_dirname, '.cache')
] : []).concat(isTeamcity ? [
  '--format', 'node\_modules/eslint-teamcity'
] : []);

Enter fullscreen mode Exit fullscreen mode

To spice things up, try the ...spread — feels a bit awkward to me, but has less syntax and makes conditional blocks stand out from the static ones:

const args = [
  ...(source !== null ? [
    source
  ] : []),
  '--ext', '.ts,.tsx,.js,.jsx',
  ...(cache ? [
    '--cache',
    '--cache-location', path.join(\_\_dirname, '.cache')
  ] : []),
  ...(isTeamcity ? [
    '--format', 'node\_modules/eslint-teamcity'
  ] : [])
];
Enter fullscreen mode Exit fullscreen mode

Truthy filtering

There’s another great option that works best when conditional fragments are single items. It’s inspired by React’s conditional rendering patterns and relies on boolean short-circuiting:

const args = [
  // here, we have either "source" or false
  source !== null && source,
  '--ext',
  '.ts,.tsx,.js,.jsx',
  cache && '--cache',
  cache && '--cache-location',
  cache && path.join(\_\_dirname, '.cache'),
  isTeamcity && '--format',
  isTeamcity && 'node\_modules/eslint-teamcity',
// filter() removes falsy items
].filter(Boolean);
Enter fullscreen mode Exit fullscreen mode

The reads like a flat array, with the important conditional logic consistently formatted to the left. Be careful, though, as this removes any falsy stuff, like empty strings and zeroes. You can work your way around it with filter(x => x !== false), but there’s no way on earth to use it on an array that can have real false values.

Best of both worlds

Developing this method further, we can combine it with the conditional concat to get the best of both worlds: ability to group several items with one condition (repeating cache && is not nice) and the conciseness of filtering:

const args = [
  source !== null && source,
  '--ext', '.ts,.tsx,.js,.jsx',
].concat(cache && [
  '--cache',
  '--cache-location', path.join(\_\_dirname, '.cache'),
]).concat(isTeamcity && [
  '--format', 'node\_modules/eslint-teamcity',
]).filter(Boolean);
Enter fullscreen mode Exit fullscreen mode

Here, we use the fact that concat has an alternate signature that accepts items, not arrays, and concat(false) just appends a false to the end of the array. If cache and isTeamcity were false, you’d end up with

const args = [
  source,
  '--ext',
  '.ts,.tsx,.js,.jsx',
  false,
  false,
]).filter(Boolean);
Enter fullscreen mode Exit fullscreen mode

And the unneeded false values would then just be filtered away. This is my personal favorite technique for building dynamic arrays. And we can apply it to strings!

Expanding to strings

Working with ES6 template strings is pleasant, but inserting fragments conditionaly is not:

const className = `btn ${isLarge ? 'btn--lg' : ''} ${isAccent ? 'btn--accent' : ''}`;
Enter fullscreen mode Exit fullscreen mode

There are two things I don’t like about this version: the : '' blocks are pretty useless, and you often get irregular whitespace around skipped items — in this case, you’d have "btn " (two extra trailing spaces) for a regular button. Luckily, we can apply the filter pattern to solve both problems:

const className = [
  'btn',
  isLarge && 'btn--lg',
  isAccent && 'btn--md'
].filter(Boolean).join(' ');
Enter fullscreen mode Exit fullscreen mode

This works even better for multiline strings:

const renderCard = ({ title, text }) => [
  `<section class="card">`,
  title && `
    <h1>${title}</h1>`,
  ` <div class="card\_\_body">
      ${text}
    </div>`,
  footer && `
    <div class="card\_\_footer">
      ${footer}
    </div>`,
  `</section>`
].filter(Boolean).join('\n');
Enter fullscreen mode Exit fullscreen mode

The formatting might seem a bit weird at first, but I honestly prefer it this way, and I built a code-generator thing that was 90% of this. Feel free to play around with indentation, though, if it’s not your cup of tea.


Today, we’ve covered three techniques to bring messy array building code back under control:

  1. Replace conditional blocks with .concat(cond ? [...data] : [])
  2. Set some array items to false via cond && item, then .filter() them away.
  3. Combine the two using concat(cond && [...data]).filter(Boolean).

You can employ these methods for building strings as well: build an array of string parts first, and join it together at the end. Good luck cleaning up your code!

Top comments (0)