All questions

classNames II

Premium

classNames II

You've already written classnames — the one-liner that absorbs strings, numbers, plain objects, and nested arrays into a single space-separated class string. This question extends it with two more capabilities that the popular libraries' dedupe mode and lazy-arg builders ship: deduplication of repeated class names, and lazy resolution of function arguments.

Dedup means a class that appears more than once in the input — from any source: a string, an object key, a nested array, the return value of a function — appears in the output only once, at its first-appearance position. Lazy function resolution means an argument that is a function isn't called eagerly during input collection — it's called when the walker reaches it, in left-to-right order, and its return value is then processed recursively as if it had been passed directly. If a function throws, the throw propagates and any later arguments are never visited.

Everything else from classnames (v1) carries over unchanged: strings are kept if truthy, numbers including 0 are kept, plain objects contribute keys whose values are truthy, arrays recurse, and null / undefined / false are dropped.

Signature

function classnames(...args: ClassValue[]): string;

type ClassValue =
  | string
  | number
  | null
  | undefined
  | false
  | { [key: string]: unknown }
  | ClassValue[]
  | (() => ClassValue);

Examples

// Dedup across plain strings — first position wins.
classnames('a', 'b', 'a');
// → 'a b'
// Dedup across input shapes — string + object key collapse to one token.
classnames('a', { b: true, a: true });
// → 'a b'
// Lazy function: invoked when reached, return value processed recursively.
classnames(() => 'x y', () => ({ y: true, z: true }));
// → 'x y z'
// Lazy function returning a falsy value is a no-op, not an error.
classnames('a', () => null, 'b');
// → 'a b'
// Function return value walks recursively — array, object, even another function.
classnames(() => ['a', { b: true }], () => () => 'c');
// → 'a b c'

Notes

  • Dedup is first-appearance-wins — the output preserves the position of each class's first occurrence, not its last. classnames('b', 'a', 'b') is 'b a', not 'a b'.
  • Lazy means lazy — a function argument is invoked exactly once, only when the walker reaches it. If an earlier argument throws, later functions are never called.
  • Function return values recurse — a function may return a string, number, object, array, another function, null, undefined, or false. All of these are processed by the same rules as if they'd been passed directly.
  • Empty string and false are still skipped; 0 is still kept — same convention as v1. classnames(0) is '0'; classnames('') is ''.
  • No throw-swallowing — if a function throws, the error propagates to the caller. Wrap your own functions in try/catch if you want soft-failure.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium