JavaScript utility functions: strings, arrays, objects
Fifteen small JavaScript helpers that keep reappearing in real projects: deduplication, deep cloning, slugs, UUIDs, compact number and plural formatting, safe JSON parsing, and email validation.
These are the functions that end up in a utils.js in every project, get rewritten slightly differently each time, and are almost never worth a dependency. Each one below is short enough to read in full before you paste it, which is the point: a fifteen-line helper you understand beats a transitive dependency you do not.
The ones with a native answer now
Several classics have been absorbed into the language, and reaching for the old workaround is now the worse choice. structuredClone handles deep copies including Map, Set, Date and cyclic references — JSON.parse(JSON.stringify(x)) loses the contents of ordinary Map and Set values, converts Date to a string and throws on cyclic references. crypto.randomUUID() produces a spec-compliant v4 UUID in every current browser and in Node. Intl.NumberFormat with notation: 'compact' turns 1234 into "1.2K" in the user's own locale, and Intl.PluralRules picks the right plural category for languages that have more than two — which is most of them outside English.
The snippets below use the native API where one exists and explain what it does not cover.
The ones where the trap is the edge case
JSON.parse throws on malformed input, so any parse of data you did not produce yourself belongs in a wrapper that returns a fallback instead of exploding. Deduplicating an array is a one-liner with Set — until the items are objects, at which point identity comparison quietly does nothing. Truncating text by character count cuts words in half; truncating by word count needs to decide what a word is. Converting "false", "0" or "" to a boolean with Boolean() gives you true for the first two, which is rarely what a config parser wants.
Validation is a matter of picking your failure mode
Email validation has no correct regular expression — the grammar in RFC 5322 permits addresses that no mail provider would accept, and any pattern short enough to read rejects valid ones. The practical position is to check for a plausible shape, reject the obviously broken, and let a confirmation email decide the rest. The snippet here takes that position deliberately rather than pretending to be exhaustive.
Slug generation has a similar shape: transliteration, diacritic stripping and collapsing separators are straightforward, but what you do with non-Latin scripts is a product decision, not a technical one.
Capitalize the first letter of a string
The short, correct way to uppercase just the first character — plus a version that capitalizes every word.
How it works: capitalize upper-cases just the first character and leaves the rest untouched; capitalizeWords uses a regex matching the first letter of every word boundary (\b\w) to title-case a whole sentence.
function capitalize(str) {
if (!str) return str
return str.charAt(0).toUpperCase() + str.slice(1)
}
function capitalizeWords(str) {
return str.replace(/\b\w/g, (char) => char.toUpperCase())
}
// Example usage
capitalize('hello world') // "Hello world"
capitalizeWords('hello world') // "Hello World"
Cosine similarity between two vectors
Measures how similar two embedding vectors are — the core operation behind semantic search and RAG retrieval.
How it works: it divides the dot product by the product of both vectors' magnitudes, so the result stays in the same -1, 1 range regardless of vector length — 1 means identical direction (very similar meaning), 0 means unrelated, -1 means opposite.
function cosineSimilarity(a, b) {
let dot = 0
let normA = 0
let normB = 0
const len = Math.min(a.length, b.length)
for (let i = 0; i < len; i++) {
dot += a[i] * b[i]
normA += a[i] * a[i]
normB += b[i] * b[i]
}
const magnitude = Math.sqrt(normA) * Math.sqrt(normB)
return magnitude === 0 ? 0 : dot / magnitude
}
// Example usage — compare a query embedding against a stored chunk
const similarity = cosineSimilarity(queryEmbedding, chunkEmbedding)
// closer to 1 = more similar
Note: if your embedding provider (e.g. OpenAI) guarantees unit-normalized vectors, a plain dot product alone already equals cosine similarity and is cheaper to compute at scale — this version handles the general case where vectors aren't pre-normalized.
Manage a deduplicated, sorted tag list
Add/remove operations for a tag array that always stays unique and alphabetically sorted — handy for tag pickers and filter chips.
How it works: addTag routes through a Set to dedupe, removeTag just filters — both re-sort before returning, so the array is always in a predictable order for rendering as chips.
function addTag(tags, tag) {
const set = new Set(tags)
set.add(tag)
return Array.from(set).sort()
}
function removeTag(tags, tag) {
return tags.filter((t) => t !== tag).sort()
}
// Example usage
let tags = ['vue', 'css']
tags = addTag(tags, 'javascript') // ['css', 'javascript', 'vue']
tags = removeTag(tags, 'css') // ['javascript', 'vue']
Deep Clone an Object
Creates a fully independent copy of an object with no shared references — uses the native structuredClone where available, falling back to a JSON round-trip for older environments.
How it works: structuredClone (Node 17+, all modern browsers) correctly copies Date, Map, Set, ArrayBuffer, and circular references. The JSON fallback is simpler but drops undefined, Date objects, functions, and special types.
function deepClone(value) {
if (typeof structuredClone === 'function') {
return structuredClone(value)
}
// Fallback for old environments (loses Date, undefined, functions)
return JSON.parse(JSON.stringify(value))
}
// Example usage
const original = { user: { name: 'Alice', tags: ['admin'] }, createdAt: new Date() }
const clone = deepClone(original)
clone.user.tags.push('editor')
console.log(original.user.tags) // ['admin'] — original untouched
Format a number compactly (1.2K, 3M)
Turns large numbers into short, human-readable labels — 600000 becomes 600K.
How it works: it checks the number's magnitude (billions, millions, thousands) and divides accordingly, trimming a trailing ".0" so "2.0M" reads as "2M".
function formatCompactNumber(n) {
const abs = Math.abs(n)
if (abs >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1).replace(/\.0$/, '')}B`
if (abs >= 1_000_000) return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`
if (abs >= 1_000) return `${(n / 1_000).toFixed(1).replace(/\.0$/, '')}K`
return `${n}`
}
// Example usage
formatCompactNumber(600000) // "600K"
formatCompactNumber(2000000) // "2M"
formatCompactNumber(1200) // "1.2K"
Generate Slug from String
Converts an arbitrary string (including Cyrillic) into a URL-friendly slug — lowercase letters, no spaces or special characters, hyphens instead of spaces.
How it works: first transliterates Cyrillic to Latin, then lowercases the string, strips all characters except letters, digits, and hyphens, and replaces whitespace with hyphens.
const CYRILLIC_MAP = {
а:'a',б:'b',в:'v',г:'g',д:'d',е:'e',ё:'yo',ж:'zh',з:'z',и:'i',
й:'j',к:'k',л:'l',м:'m',н:'n',о:'o',п:'p',р:'r',с:'s',т:'t',
у:'u',ф:'f',х:'kh',ц:'ts',ч:'ch',ш:'sh',щ:'shch',ъ:'',ы:'y',ь:'',
э:'e',ю:'yu',я:'ya',
}
function transliterate(str) {
return str
.split('')
.map((ch) => CYRILLIC_MAP[ch.toLowerCase()] ?? ch)
.join('')
}
function generateSlug(str) {
return transliterate(str)
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_]+/g, '-')
.replace(/^-+|-+$/g, '')
}
// Example usage
generateSlug('Hello World 2026') // 'hello-world-2026'
generateSlug(' --foo bar-- ') // 'foo-bar'
Generate a UUID
Creates a spec-compliant random UUID (v4) using the built-in Web Crypto API — no external library needed.
How it works: crypto.randomUUID() (Node 14.17+, all modern browsers) is cryptographically random and RFC 4122 v4-compliant; the fallback reproduces the same format for older runtimes but is NOT cryptographically secure.
function generateUuid() {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID()
}
// Fallback for environments without crypto.randomUUID (older Node/browsers)
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0
const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}
// Example usage
generateUuid() // "3f9a1c2e-8b3d-4c1a-9f2e-6d8b1a4c7e9f"
Pick and omit object keys
Two small helpers for building a new object with only (or without) a specific set of keys — handy for trimming payloads before sending them to an API.
How it works: pickKeys keeps only the requested keys, omitKeys keeps everything except them — both build a fresh object rather than mutating the original.
function pickKeys(obj, keys) {
if (!obj) return {}
return keys.reduce((acc, key) => {
if (key in obj) acc[key] = obj[key]
return acc
}, {})
}
function omitKeys(obj, keys) {
if (!obj) return {}
return Object.entries(obj).reduce((acc, [key, value]) => {
if (!keys.includes(key)) acc[key] = value
return acc
}, {})
}
// Example usage
const user = { id: 1, name: 'Alex', password: 'secret', email: '[email protected]' }
pickKeys(user, ['id', 'name']) // { id: 1, name: 'Alex' }
omitKeys(user, ['password']) // { id: 1, name: 'Alex', email: '[email protected]' }
Pluralize words with Intl.PluralRules
Correctly pluralizes nouns for any locale — including languages like Russian with multiple plural forms — using the built-in Intl API.
How it works: Intl.PluralRules classifies a count into a plural category ('one', 'few', 'many', 'other', ...) per locale rules; you map each category to the right word form instead of hardcoding an "add s" rule that only works for English.
function pluralize(count, singular, options = {}) {
const { locale = 'en-US', plural, forms = {} } = options
const category = new Intl.PluralRules(locale).select(count)
const word = forms[category] || (category === 'one' ? singular : plural || `${singular}s`)
return `${count} ${word}`
}
// Example usage
pluralize(1, 'apple') // "1 apple"
pluralize(5, 'apple') // "5 apples"
// Russian needs multiple forms (one/few/many)
pluralize(3, 'яблоко', {
locale: 'ru-RU',
forms: { one: 'яблоко', few: 'яблока', many: 'яблок' }
}) // "3 яблока"
Remove duplicates from an array
The one-line way to deduplicate an array of primitives using Set, plus the pattern for deduplicating an array of objects by a key.
How it works: Set only stores unique values, so spreading it back into an array dedupes primitives in one line; for objects (which are never "equal" to each other by reference), you instead track seen key values manually while filtering.
// Primitives — numbers, strings, etc.
const unique = [...new Set([1, 2, 2, 3, 3, 3])] // [1, 2, 3]
// Objects — dedupe by a specific key
function uniqueByKey(items, key) {
const seen = new Set()
return items.filter((item) => {
if (seen.has(item[key])) return false
seen.add(item[key])
return true
})
}
// Example usage
uniqueByKey(
[{ id: 1, name: 'A' }, { id: 2, name: 'B' }, { id: 1, name: 'A (dup)' }],
'id'
) // [{ id: 1, name: 'A' }, { id: 2, name: 'B' }]
A JSON.parse that never throws
Wraps JSON.parse to return null on invalid input instead of crashing — perfect for deserializing values from Redis, localStorage, or user input.
How it works: null/undefined/empty-string input is treated as "no value" and returns null immediately; anything else goes through a try/catch so malformed JSON returns null instead of throwing.
function safeJsonParse(raw) {
if (raw === null || raw === undefined || raw === '') return null
try {
return JSON.parse(raw)
} catch {
return null
}
}
// Example usage — instead of a try/catch at every call site
const session = safeJsonParse(await redis.get('session:123'))
if (!session) {
// treat as "no session" rather than crashing
}
Parse loose values into a real boolean
Turns strings like "yes"/"on"/"1" (and numbers) into a proper boolean — handy for parsing env vars, query params, or CSV imports.
How it works: string values are lowercased and trimmed, then checked against configurable truthy/falsy word lists; numbers are truthy unless they're 0 or NaN; anything else falls back to plain Boolean(value).
function toBoolean(value, options = {}) {
const {
truthyValues = ['true', 'yes', 'on', 'y', '1'],
falsyValues = ['false', 'no', 'off', 'n', '0', '']
} = options
if (typeof value === 'string') {
const normalized = value.toLowerCase().trim()
if (truthyValues.includes(normalized)) return true
if (falsyValues.includes(normalized)) return false
}
if (typeof value === 'number') return value !== 0 && !Number.isNaN(value)
return Boolean(value)
}
// Example usage
toBoolean('yes') // true
toBoolean('0') // false
toBoolean(process.env.FEATURE_FLAG) // works great for env vars
Truncate Text by Word Count
Truncates a string to a given number of words and appends a suffix ("…" by default). Unlike character truncation, it never splits a word in the middle.
How it works: splits the string on whitespace, counts words. If the word count is within the limit, returns the original unchanged. Otherwise slices to the desired word count and appends the suffix.
function truncateWords(text, maxWords, suffix = '…') {
const words = text.trim().split(/\s+/)
if (words.length <= maxWords) return text
return words.slice(0, maxWords).join(' ') + suffix
}
function truncateChars(text, maxChars, suffix = '…') {
if (text.length <= maxChars) return text
return text.slice(0, maxChars).trimEnd() + suffix
}
// Example usage
truncateWords('The quick brown fox jumped', 3) // 'The quick brown…'
truncateWords('Short', 10) // 'Short'
truncateChars('Hello World', 7) // 'Hello W…'
Validate Email Address
A basic email-shape check for forms. It does not cover all valid syntax or confirm that an address exists.
The regex checks a common address shape with @ and a dotted domain. It is a heuristic: it can reject unusual valid addresses and accept invalid ones. The example counts JavaScript string units, not SMTP byte limits. Send a confirmation email to verify ownership. The ASCII variant below is not a complete standards validator either.
function isValidEmail(email) {
if (!email || typeof email !== 'string') return false
const trimmed = email.trim()
if (trimmed.length > 254) return false
return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(trimmed)
}
// Simplified ASCII variant; IDNs may be represented in Punycode
function isValidEmailStrict(email) {
const re = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/
return re.test((email ?? '').trim().toLowerCase())
}
// Example usage
isValidEmail('[email protected]') // true
isValidEmail('[email protected]') // true
isValidEmail('notanemail') // false
isValidEmail('missing@tld') // false
Validate a media URL
Checks that a URL is HTTPS and points to a recognized image/video/audio file extension — a common guard before accepting a user-supplied media link.
How it works: isValidHttpsUrl relies on the URL constructor throwing on malformed input rather than hand-rolling a regex; isMediaUrl checks the extension against a known list, also allowing for a trailing query string (photo.jpg?size=large).
function isValidHttpsUrl(url) {
if (!url) return false
try {
return new URL(url).protocol === 'https:'
} catch {
return false
}
}
function isMediaUrl(url) {
if (!url) return false
const extensions = ['.jpg', '.jpeg', '.png', '.gif', '.mp4', '.mp3', '.ogg', '.wav']
const lower = url.toLowerCase()
return extensions.some((ext) => lower.endsWith(ext) || lower.includes(`${ext}?`))
}
// Example usage
isValidHttpsUrl('https://example.com/photo.jpg') && isMediaUrl('https://example.com/photo.jpg')
// true — safe to treat as a media attachment
More snippet collections
Contact Information
Submit a request or write to me directly on Telegram. I'll reply within 24 hours.