← Daily Logs
GreatFrontEnd

Implement classNames

MediumJul 1, 2026recursionutilityobjects

Rebuild the `classnames`/`clsx` utility that every React app leans on — join a messy mix of strings, condition objects, and nested arrays into one clean class string. The whole trick is deciding what each argument *is* and recursing when it's a container.

The problem

Write `classNames(...args)` that joins its arguments into a space-separated class string. A string passes straight through; an object contributes each key whose value is truthy (`{ foo: true, bar: false }` → `'foo'`); and arrays are flattened recursively so `['a', { b: true }]` behaves like the same values passed at the top level.

Two edge rules do the real work. Every falsy argument — `null`, `undefined`, `false`, `''`, `0` — is ignored entirely, and the returned string must have no leading, trailing, or doubled whitespace. That means falsy values can never reach the final `join`, or they'd leave stray gaps.

The approach

Walk the arguments and branch on type. Skip anything falsy up front with `if (!arg) continue` — that single guard handles every ignore-me case in one line and, as a bonus, filters out `null` before the `typeof` check (since `typeof null === 'object'` would otherwise misroute it into the object branch).

For arrays, recurse with `classNames(...arg)` and push the result only if it's non-empty — that's what makes nested objects and deeper arrays get the exact same treatment as top-level ones. For plain objects, loop the keys and push each one whose value is truthy. Everything else is a string or number, so push it directly.

Collecting into an array and joining with a single space at the end is what keeps the whitespace clean: because falsy values are dropped before they're collected, `result.join(' ')` never has an empty slot to pad, so no trailing or double spaces can sneak in.

The solution

js
/**
 * @param {...(any|Object|Array<any|Object|Array>)} args
 * @return {string}
 */
export default function classNames(...args) {
  const result = [];

  for (const arg of args) {
    if (!arg) continue; // ignore falsy: null, undefined, false, '', 0, NaN

    if (Array.isArray(arg)) {
      const inner = classNames(...arg); // recurse, flatten any depth
      if (inner) result.push(inner);
    } else if (typeof arg === "object") {
      for (const key in arg) {
        if (arg[key]) result.push(key);
      }
    } else {
      result.push(arg); // string or number
    }
  }

  return result.join(" ");
}

Time O(n)Space O(n)

All entries