Medium

classNames

Prompt

Implement a classNames function that conditionally joins CSS class names together. It should accept any number of arguments, which can be strings, objects, or arrays.

  • Strings are added directly
  • For objects, keys with truthy values are added
  • Arrays are processed recursively
  • All falsy values (null, undefined, false, 0, '') are ignored
classNames('foo', 'bar'); // 'foo bar'
classNames('foo', { bar: true }); // 'foo bar'
classNames({ foo: true, bar: false }); // 'foo'
classNames('a', ['b', { c: true }]); // 'a b c'
classNames(null, false, 'bar', undefined); // 'bar'

Playground

Hint 1

Loop through each argument and check its type. Strings get added directly. Objects need their keys checked. Arrays need to be processed the same way as the top-level arguments (that's recursion).

Hint 2

Skip any argument that's falsy right away (if (!arg) return). This handles null, undefined, false, 0, and '' in one check. For objects, only include keys where the value is truthy.

Hint 3

Be careful with arrays: typeof [] returns 'object', so check Array.isArray(arg) before checking for objects. For arrays, you can recursively call classNames(...arg) and add the result.

Solution

Explanation

This is a real-world utility. The classnames npm package has over 10 million weekly downloads. It's used constantly in React projects to conditionally apply CSS classes. Knowing how it works under the hood is a great interview question.

The approach

We collect valid class names into an array, then join them with spaces at the end. For each argument, we figure out what type it is and handle it accordingly:

function classNames(...args) {
const classes = [];

for (const arg of args) {
if (!arg) continue;

const type = typeof arg;

if (type === 'string' || type === 'number') {
classes.push(arg);
} else if (Array.isArray(arg)) {
const inner = classNames(...arg);
if (inner) classes.push(inner);
} else if (type === 'object') {
for (const key of Object.keys(arg)) {
if (arg[key]) classes.push(key);
}
}
}

return classes.join(' ');
}

Walking through each type

Falsy values (null, undefined, false, 0, '') are skipped immediately with if (!arg) continue. This is the first check, so we never have to worry about these later.

Strings and numbers are added directly to the classes array. Numbers are included because they get converted to strings when joined.

Arrays are handled recursively. We spread the array elements as arguments to classNames itself: classNames(...arg). This means ['a', 'b'] gets processed as if someone called classNames('a', 'b'). The recursive result is a string, and we add it to our collection if it's not empty.

Objects are the interesting part. We loop through the object's keys and check if the value is truthy. If { active: true, disabled: false } is passed, only 'active' gets added because true is truthy and false is not.

Why check Array.isArray before typeof object?

In JavaScript, typeof [] returns 'object'. If we checked for objects first, arrays would be treated as objects and we'd iterate over their numeric indices as keys (like '0', '1', '2'). That's not what we want. So we check Array.isArray(arg) first, and only fall through to the object case if it's not an array.

Why this order matters

The order of our type checks is deliberate:

  1. Falsy check first (handles null, which is typeof 'object')
  2. String/number check
  3. Array check (before object, because arrays are objects)
  4. Object check last

If we moved the falsy check after the object check, null would pass the typeof arg === 'object' test (because typeof null === 'object' is one of JavaScript's famous quirks) and we'd try to call Object.keys(null), which throws an error.

In real React projects, you use this function like: classNames('btn', { 'btn-primary': isPrimary, 'btn-disabled': isDisabled }). This produces 'btn btn-primary' when isPrimary is true and isDisabled is false. It's much cleaner than building class strings with template literals and ternary operators.