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.
function classnames(...args: ClassValue[]): string;
type ClassValue =
| string
| number
| null
| undefined
| false
| { [key: string]: unknown }
| ClassValue[]
| (() => ClassValue);
// 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'
classnames('b', 'a', 'b') is 'b a', not 'a b'.null, undefined, or false. All of these are processed by the same rules as if they'd been passed directly.false are still skipped; 0 is still kept — same convention as v1. classnames(0) is '0'; classnames('') is ''.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.