Format a list with commas 'and' in JavaScript
Join all but the last item with commas, then append the last with "and". Handle the empty and single-item cases first, because both break the general rule. Options such as sorted and unique are applied to a copy before formatting so the input is never modified, and length then shows only that many names, summarising the rest as "and 2 others".
The question
A gentle question that rewards care. Every candidate writes the two-item and three-item cases
correctly; the ones who stand out handle zero and one, and notice that sort()
mutates the array it’s called on.
Try implementing it before reading the solution. The JavaScript interview practice editor lets you write your answer and run the question’s tests.
The solution
The copy on the first line is doing real work: sort() sorts in place, so without it
the caller's array would be reordered.
function listFormat(items, options = {}) {
let values = [...items];
if (options.unique) values = [...new Set(values)];
if (options.sorted) values = values.sort();
if (values.length === 0) return "";
if (values.length === 1) return String(values[0]);
// With a length, show that many names and count the rest as the final item.
if (options.length > 0 && options.length < values.length) {
const hidden = values.length - options.length;
const shown = values.slice(0, options.length);
return shown.join(", ") + " and " + hidden + (hidden === 1 ? " other" : " others");
}
const last = values[values.length - 1];
const rest = values.slice(0, -1);
return rest.join(", ") + " and " + last;
}How it works
- The input is copied immediately, because
sort()mutates. This is the single most common bug in this question. - Unique runs before sorted, so deduplication can’t reintroduce ordering surprises.
- Zero and one items are special-cased, since there’s no separator to place in either.
- Everything but the last joins with ", " and the last is appended after " and ", which is the whole formatting rule.
The tests it has to pass
This is the part most explanations leave out. "It works" is not a claim you can check by reading. These are the 6 cases Practice Pad runs against your solution, and the implementation above passes all of them.
✓ joins names with commas and a final "and"
expectEqual(listFormat(["Ada", "Grace", "Linus"]), "Ada, Grace and Linus");
expectEqual(listFormat(["Ada", "Linus"]), "Ada and Linus");✓ applies the sorted option on its own
expectEqual(listFormat(["Linus", "Ada", "Grace"], { sorted: true }), "Ada, Grace and Linus");✓ applies the unique option on its own
expectEqual(listFormat(["Ada", "Linus", "Ada"], { unique: true }), "Ada and Linus");✓ returns an empty string for no names and the name itself for one
expectEqual(listFormat([]), "");
expectEqual(listFormat(["Ada"]), "Ada");✓ shows only the requested number of names and counts the rest
expectEqual(listFormat(["Ada", "Grace", "Linus", "Mae"], { length: 2 }), "Ada, Grace and 2 others");
expectEqual(listFormat(["Ada", "Grace", "Linus"], { length: 2 }), "Ada, Grace and 1 other");
expectEqual(listFormat(["Ada", "Grace"], { length: 5 }), "Ada and Grace");✓ applies unique, then sorted, then length together
expectEqual(listFormat(["Ada", "Linus", "Grace", "Ada"], { unique: true, sorted: true, length: 2 }), "Ada, Grace and 1 other");Edge cases to watch out for
Letting sort() mutate the caller's array
Calling items.sort() reorders the array that was passed in. Copying first with a spread costs one line and prevents an action-at-a-distance bug that’s genuinely hard to track down.
Forgetting the zero and one item cases
The general rule produces " and Ada" for a single item and " and undefined" for none. Both need handling before the main path.
Joining then replacing the last comma
Building the whole string and then using lastIndexOf(",") to swap in "and" breaks the moment an item itself contains a comma. Slice the array instead.
new Intl.ListFormat("en", { style: "long", type: "conjunction" }).format(items) does this, and handles the Oxford comma and every other locale's conventions correctly. Implementing it by hand is the exercise; knowing the built-in is the professional answer.Follow up questions
How do you handle "or" instead of "and"?
Make the conjunction a parameter rather than hard-coding it. That’s exactly what Intl.ListFormat does with its type option: "conjunction" for and, "disjunction" for or, "unit" for a plain list with no final word at all.
What is Intl.ListFormat?
A built-in that formats lists according to locale conventions, including whether to use a serial comma and what the local equivalent of "and" or "or" is. new Intl.ListFormat("en").format(["a","b","c"]) gives "a, b, and c".
Why copy the array before sorting?
Because Array.prototype.sort sorts in place and returns the same array. Sorting the argument directly reorders the caller's data as a side effect, which is exactly the kind of bug that’s hard to trace back. toSorted() is the newer non-mutating form.
Related interview questions
- Implement classnames in JavaScript — Recursion over mixed argument types, filtering falsy.
- Filter an array of objects in JavaScript — Compose optional filters, and decide what runs first.
- Flatten a nested object in JavaScript — Turn nested structure into dot-separated key paths.
Practise this with the tests running
Practice Pad runs these 6 cases against your own solution, in a real editor, in your browser or on your desktop. Twenty-four questions, and an AI panel that explains why a test failed.