anymany API reference
Overview
anymany is a list formatter built entirely on the native Intl browser API. One function, one options object. Sorted right, joined right, in any locale. Stable since 1.0 — the public API follows semver.
The browser already knows how to join and collate lists in 200+ languages. anymany just makes that API pleasant to use.
import { anymany } from 'anymany'
anymany(['banana', 'apple', 'cherry'])
// "banana, apple, and cherry"
anymany(['S', 'M', 'L'], { type: 'disjunction' })
// "S, M, or L"
anymany(['x', 'y', 'z', 'a', 'b', 'c', 'd'], { max: 3 })
// "x, y, z, and +4"Install
npm install anymany
# or
pnpm add anymany
# or
yarn add anymanyOr take the whole family at once with npm install anyfamily.
anymany()
The single entry point. Pass the items — an array, a Set, any iterable — and optionally options. Non-string items are coerced via String(). An empty array returns ""; a single item is returned as-is.
anymany(items)
anymany(items, options?)
anymany(['read', 'write'])
// "read and write"
anymany(['a', 'b', 'c'], { style: 'short' })
// "a, b, & c"
anymany(['4 kg', '2 m'], { type: 'unit' })
// "4 kg, 2 m"
anymany(['cherry', 'apple', 'Banana'], { sort: true })
// "apple, Banana, and cherry"Migrating from 1.x
2.0 removed the separate anymanyParts— they are the same functions and values, reached through the one name the package exports.
- import { anymany, anymanyParts } from 'anymany'
+ import { anymany } from 'anymany'
- anymanyParts(items)
+ anymany.parts(items)Arguments, return values and throwing behaviour are unchanged, and nothing else in the API moved. Every any* package follows this shape from 2.0 on: the bare call does the job, everything else hangs off the same name.
anymany.parts()
Same arguments as anymany(), but returns the output as { type, value } parts instead of a string — style the items apart from the separators, or rebuild the output your own way.
import { anymany } from 'anymany'
anymany.parts(['a', 'b'])
// [
// { type: 'element', value: 'a' },
// { type: 'literal', value: ' and ' },
// { type: 'element', value: 'b' },
// ]
// React: bold the items
anymany.parts(tags).map((p, i) =>
p.type === 'element' ? <b key={i}>{p.value}</b> : p.value,
)Sorting
sort runs the items through Intl.Collator before joining — real language-aware collation, not code-point order. The input array is never mutated.
anymany(['cherry', 'apple', 'Banana'], { sort: true })
// "apple, Banana, and cherry" ← plain .sort() puts "Banana" first
anymany(['file10', 'file2'], { sort: 'numeric' })
// "file2 and file10" ← numbers compared by value
anymany(['a', 'A'], { sort: { caseFirst: 'upper' } })
// "A and a" ← any Intl.CollatorOptionsMax + overflow
max caps the visible items; the rest collapse into a trailing "+N" counter. Digits come from Intl.NumberFormat, so they localize — no words, locale-safe.
anymany(['x', 'y', 'z', 'a', 'b', 'c', 'd'], { max: 3 })
// "x, y, z, and +4"
anymany(['x', 'y', 'z', 'a', 'b'], { max: 3, overflow: (n) => `${n} more` })
// "x, y, z, and 2 more"The overflow item is just another list element, so the default typeplaces an "and" before it. That is intentional — no hidden joiner magic. Prefer a plain comma list? Combine max with type: 'unit'.
Options
localestring | string[]default: runtime localeAny valid BCP 47 locale tag, or a fallback array — 'en', 'en-US', 'zh-TW', ['sr-Latn-RS', 'en'].
type'conjunction' | 'disjunction' | 'unit'default: 'conjunction'List flavor, mapped to Intl.ListFormat — 'a, b, and c' / 'a, b, or c' / 'a, b, c'.
style'long' | 'short' | 'narrow'default: 'long'Joiner wording length, mapped to Intl.ListFormat — 'and' / '&' / none.
sortboolean | 'numeric' | Intl.CollatorOptionsdefault: no sortingSort items with Intl.Collator before joining. true = default collation, 'numeric' = numeric collation, or any Intl.CollatorOptions for full control. Never mutates the input.
maxnumberdefault: no limitMaximum items to show (after sorting). The rest collapse into a trailing '+N' counter with localized digits. Throws RangeError when zero, negative, or fractional.
overflow(hidden: number) => stringdefault: `+${N}`Custom overflow label builder, replaces the default '+N'. Receives the number of hidden items.
What breaks without this
Every one of these is a line people write by hand, and each is wrong somewhere.
join(', ') plus ' and ' at the end
"and" is not a word every locale has in that position. Spanish switches y to e before a word starting with i or hi; Japanese joins with a particle, not a conjunction. The connector is data, not a string constant.
The Oxford comma is a locale fact
en-US writes "a, b, and c", en-GB writes "a, b and c". Same language, different list. Whichever one you hardcoded is wrong for half your English readers.
sort() sorts by code unit
['ä', 'z', 'a'].sort() puts ä after z, because that is the order of their code points and nothing else. Swedish files å ä ö after z on purpose; German does not. Collation is a locale rule, and Array.prototype.sort has never heard of it.
An or-list is not an and-list with a word swapped
"a, b or c" is a different list type, and some locales punctuate it differently. So is a bare unit list — "3 ft 7 in" joins without any connector at all.
Recipes
Copy, paste, move on.
// Tag list
anymany(post.tags, { locale: 'en' })
// "design, typography, and color"
// Sizes / options — "or" instead of "and"
anymany(product.sizes, { type: 'disjunction' })
// "S, M, or L"
// Plain comma list, no joiner word
anymany(['4 kg', '2 m'], { type: 'unit' })
// "4 kg, 2 m"
// Alphabetical the way the language actually orders letters
anymany(names, { sort: true, locale: 'de' })
// "Apfel, Öl und Zebra"
// Filenames with numbers, ordered by value
anymany(files, { sort: 'numeric' })
// "file2 and file10"
// Cap a long list
anymany(participants, { max: 3 })
// "Ann, Bob, Cy, and +4"
// …with your own overflow wording
anymany(participants, { max: 3, overflow: (n) => `${n} more` })
// "Ann, Bob, Cy, and 2 more" React / Next.js
anymany is pure and synchronous, so it works in a component as-is. Whatanyfamily-react adds is a shared locale: set it once onAnyfamilyProvider and every hook below picks it up, so you do not thread locale through every call.
import { AnyfamilyProvider, useAnymany } from 'anyfamily-react'
function Tags({ tags }: { tags: string[] }) {
return <p>{useAnymany(tags, { sort: true, max: 5 })}</p>
}
<AnyfamilyProvider locale="en">
<Tags tags={post.tags} />
</AnyfamilyProvider>Locales
Same calls in a few languages — no extra setup, no locale files.
anymany(['a', 'b', 'c'], { locale: 'en' }) // "a, b, and c"
anymany(['a', 'b', 'c'], { locale: 'ru' }) // "a, b и c"
anymany(['a', 'b', 'c'], { locale: 'de' }) // "a, b und c"
anymany(['a', 'b', 'c'], { locale: 'ja' }) // "a、b、c"
anymany(['a', 'b', 'c'], { type: 'disjunction', locale: 'ru' })
// "a, b или c"
anymany(['Öl', 'Zebra', 'Apfel'], { sort: true, locale: 'de' })
// "Apfel, Öl und Zebra" ← Ö sorts after A, not after Z
anymany(['файл10', 'файл2'], { sort: 'numeric', locale: 'ru' })
// "файл2 и файл10"
anymany(['a', 'b', 'c', 'd', 'e', 'f', 'g'], { max: 3, locale: 'ar-EG' })
// "a وb وc و+٤" ← localized overflow digitsPass any valid BCP 47 language tag — including regional variants like en-GB, zh-TW, or pt-BR. Locale is optional; when omitted, native Intl uses the runtime locale. Fallback arrays like ['sr-Latn-RS', 'en'] also work.
SSR
anymany is pure — same input, same output, no clocks, no randomness, no DOM. Server and client render identically as long as the locale is passed explicitly (the runtime locale may differ between server and browser).
import { anymany } from 'anymany'
export function TagList({ tags }: { tags: string[] }) {
return <p>{anymany(tags, { locale: 'en', sort: true, max: 5 })}</p>
}vs the alternatives
What you would otherwise reach for, and what changes if you do.
| anymany | join() by hand | Intl.ListFormat direct | |
|---|---|---|---|
| locale data bundled | none (Intl) | none | none (Intl) |
| connector from the locale | yes | no | yes |
| and / or / unit lists | yes | no | yes |
| collation-aware sort | yes | no | no |
| overflow to a rest count | yes | no | no |
| parts, for styling | yes | no | yes |
| dependencies | 0 | 0 | 0 |
The honest comparison for anymany is the native API rather than a library, because there is barely a library to compare against. At 0.7kb gzipped it adds the sorting, the overflow and the fallback chain around Intl.ListFormat, and saves you constructing a formatter per call.
Compatibility
anymany uses Intl.ListFormat, Intl.Collator, and Intl.NumberFormat — all widely supported.
Limitations
A few things worth knowing before you ship:
Output depends on the runtime's Intl data
anymany delegates all formatting to native Intl. Exact output — joiner words, comma placement, the Oxford comma — may vary between Node versions, browsers, and regional variants (en vs en-GB). Don't hardcode expected strings in tests; use pattern matching instead.
No pluralization, by design
Intl ships no word data, and anymany ships zero language dictionaries — that is what keeps it lightweight and correct in every locale. The overflow counter is '+N' (localized digits) instead of 'and N more'. Need words? Pass your own via the overflow callback.
The overflow item is a regular list element
With max set, the '+N' counter goes through Intl.ListFormat like any other item, so conjunction mode reads 'x, y, z, and +4'. No hidden joiner magic — combine max with type: 'unit' for a plain comma list.
Node.js < 18
The package declares engines.node >= 18 and CI tests Node 20/22/24. Older versions down to 13 will usually work — the required Intl APIs are there — but they are unsupported and untested.