nano-string-utils
TypeScript icon, indicating that this package has built-in type declarations

0.9.1 • Public • Published

nano-string-utils

Ultra-lightweight string utilities with zero dependencies. Tree-shakeable, fully typed, and optimized for modern JavaScript.

npm version JSR Bundle Size TypeScript CI/CD License: MIT

Features

  • 🚀 Zero dependencies - No bloat, just pure functions
  • 📦 < 1KB per function - Minimal bundle impact
  • 🌳 Tree-shakeable - Only import what you need
  • 💪 Fully typed - Complete TypeScript support with function overloads and template literal types
  • Fast performance - 2-25x faster than lodash for many operations
  • ESM & CJS - Works everywhere
  • 🧪 100% tested - Reliable and production-ready
  • 🔒 Type-safe - Written in strict TypeScript with enhanced type inference and compile-time transformations
  • 🛡️ Null-safe - All functions handle null/undefined gracefully without throwing errors
  • 📝 Well documented - JSDoc comments for all functions

Installation

From npm

npm install nano-string-utils
yarn add nano-string-utils
pnpm add nano-string-utils

From JSR

npx jsr add @zheruel/nano-string-utils
deno add @zheruel/nano-string-utils

Usage

ESM

import { slugify, camelCase, truncate } from "nano-string-utils";

slugify("Hello World!"); // 'hello-world'
camelCase("hello-world"); // 'helloWorld'
truncate("Long text here", 10); // 'Long te...'

CommonJS

const { slugify, camelCase, truncate } = require("nano-string-utils");

slugify("Hello World!"); // 'hello-world'
camelCase("hello-world"); // 'helloWorld'
truncate("Long text here", 10); // 'Long te...'

API Reference

String Transformation

template(str: string, data: Record<string, any>, options?: TemplateOptions): string

Interpolates variables in a template string.

template("Hello {{name}}!", { name: "World" }); // 'Hello World!'
template("{{user.name}} is {{user.age}}", {
  user: { name: "Alice", age: 30 },
}); // 'Alice is 30'
template(
  "Hello ${name}!",
  { name: "World" },
  {
    delimiters: ["${", "}"],
  }
); // 'Hello World!'

templateSafe(str: string, data: Record<string, any>, options?: TemplateOptions): string

Interpolates variables with HTML escaping for safe output.

templateSafe("Hello {{name}}!", {
  name: '<script>alert("XSS")</script>',
}); // 'Hello &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;!'

slugify(str: string): string

Converts a string to a URL-safe slug.

slugify("Hello World!"); // 'hello-world'
slugify("  Multiple   Spaces  "); // 'multiple-spaces'

camelCase(str: string): string

Converts a string to camelCase.

camelCase("hello world"); // 'helloWorld'
camelCase("hello-world"); // 'helloWorld'
camelCase("hello_world"); // 'helloWorld'

snakeCase(str: string): string

Converts a string to snake_case.

snakeCase("hello world"); // 'hello_world'
snakeCase("helloWorld"); // 'hello_world'
snakeCase("hello-world"); // 'hello_world'

kebabCase(str: string): string

Converts a string to kebab-case.

kebabCase("hello world"); // 'hello-world'
kebabCase("helloWorld"); // 'hello-world'
kebabCase("hello_world"); // 'hello-world'

pascalCase(str: string): string

Converts a string to PascalCase.

pascalCase("hello world"); // 'HelloWorld'
pascalCase("hello-world"); // 'HelloWorld'
pascalCase("hello_world"); // 'HelloWorld'

constantCase(str: string): string

Converts a string to CONSTANT_CASE.

constantCase("hello world"); // 'HELLO_WORLD'
constantCase("helloWorld"); // 'HELLO_WORLD'
constantCase("hello-world"); // 'HELLO_WORLD'
constantCase("XMLHttpRequest"); // 'XML_HTTP_REQUEST'

dotCase(str: string): string

Converts a string to dot.case.

dotCase("hello world"); // 'hello.world'
dotCase("helloWorld"); // 'hello.world'
dotCase("hello-world"); // 'hello.world'
dotCase("XMLHttpRequest"); // 'xml.http.request'
dotCase("com/example/package"); // 'com.example.package'

capitalize(str: string): string

Capitalizes the first letter of a string and lowercases the rest.

capitalize("hello world"); // 'Hello world'
capitalize("HELLO"); // 'Hello'

reverse(str: string): string

Reverses a string.

reverse("hello"); // 'olleh'
reverse("world"); // 'dlrow'

deburr(str: string): string

Removes diacritics/accents from Latin characters.

deburr("café"); // 'cafe'
deburr("naïve"); // 'naive'
deburr("Bjørn"); // 'Bjorn'
deburr("São Paulo"); // 'Sao Paulo'
deburr("Müller"); // 'Muller'

titleCase(str: string, options?: { exceptions?: string[] }): string

Converts a string to title case with proper capitalization rules.

titleCase("the quick brown fox"); // 'The Quick Brown Fox'
titleCase("a tale of two cities"); // 'A Tale of Two Cities'
titleCase("mother-in-law"); // 'Mother-in-Law'
titleCase("don't stop believing"); // "Don't Stop Believing"
titleCase("NASA launches rocket"); // 'NASA Launches Rocket'
titleCase("2001: a space odyssey"); // '2001: A Space Odyssey'

// With custom exceptions
titleCase("the lord of the rings", {
  exceptions: ["versus"],
}); // 'The Lord of the Rings'

sentenceCase(str: string): string

Converts a string to sentence case (first letter of each sentence capitalized).

sentenceCase("hello world"); // 'Hello world'
sentenceCase("HELLO WORLD"); // 'Hello world'
sentenceCase("hello. world! how are you?"); // 'Hello. World! How are you?'
sentenceCase("this is the first. this is the second."); // 'This is the first. This is the second.'
sentenceCase("the u.s.a. is large"); // 'The u.s.a. is large'
sentenceCase("i love javascript"); // 'I love javascript'
sentenceCase("what? when? where?"); // 'What? When? Where?'

pathCase(str: string): string

Converts a string to path/case (forward slash separated).

pathCase("hello world"); // 'hello/world'
pathCase("helloWorld"); // 'hello/world'
pathCase("hello-world"); // 'hello/world'
pathCase("hello_world"); // 'hello/world'
pathCase("XMLHttpRequest"); // 'xml/http/request'
pathCase("src.components.Header"); // 'src/components/header'
pathCase("com.example.package"); // 'com/example/package'

String Manipulation

truncate(str: string, length: number, suffix?: string): string

Truncates a string to a specified length with an optional suffix.

truncate("Long text here", 10); // 'Long te...'
truncate("Long text here", 10, "→"); // 'Long tex→'

stripHtml(str: string): string

Removes HTML tags from a string.

stripHtml("<p>Hello <b>world</b>!</p>"); // 'Hello world!'
stripHtml("<div>Text</div>"); // 'Text'

escapeHtml(str: string): string

Escapes HTML special characters.

escapeHtml('<div>Hello & "world"</div>'); // '&lt;div&gt;Hello &amp; &quot;world&quot;&lt;/div&gt;'
escapeHtml("It's <b>bold</b>"); // 'It&#x27;s &lt;b&gt;bold&lt;/b&gt;'

excerpt(str: string, length: number, suffix?: string): string

Creates a smart excerpt from text with word boundary awareness.

excerpt("The quick brown fox jumps over the lazy dog", 20); // 'The quick brown fox...'
excerpt("Hello world. This is a test.", 15); // 'Hello world...'
excerpt("Long technical documentation text here", 25, "…"); // 'Long technical…'
excerpt("Supercalifragilisticexpialidocious", 10); // 'Supercalif...'

wordCount(str: string): number

Counts the number of words in a string.

wordCount("Hello world test"); // 3
wordCount("One-word counts as one"); // 5

normalizeWhitespace(str: string, options?: NormalizeWhitespaceOptions): string

Normalizes various Unicode whitespace characters to regular spaces.

normalizeWhitespace("hello   world"); // 'hello world'
normalizeWhitespace("hello\u00A0world"); // 'hello world' (non-breaking space)
normalizeWhitespace("  hello  "); // 'hello'
normalizeWhitespace("hello\n\nworld"); // 'hello world'

// With options
normalizeWhitespace("  hello  ", { trim: false }); // ' hello '
normalizeWhitespace("a    b", { collapse: false }); // 'a    b'
normalizeWhitespace("hello\n\nworld", { preserveNewlines: true }); // 'hello\n\nworld'

// Handles various Unicode spaces
normalizeWhitespace("café\u2003test"); // 'café test' (em space)
normalizeWhitespace("hello\u200Bworld"); // 'hello world' (zero-width space)
normalizeWhitespace("日本\u3000語"); // '日本 語' (ideographic space)

removeNonPrintable(str: string, options?: RemoveNonPrintableOptions): string

Removes non-printable control characters and formatting characters from strings.

removeNonPrintable("hello\x00world"); // 'helloworld' (removes NULL character)
removeNonPrintable("hello\nworld"); // 'helloworld' (removes newline by default)
removeNonPrintable("hello\u200Bworld"); // 'helloworld' (removes zero-width space)
removeNonPrintable("hello\u202Dworld"); // 'helloworld' (removes directional override)

// With options
removeNonPrintable("hello\nworld", { keepNewlines: true }); // 'hello\nworld'
removeNonPrintable("hello\tworld", { keepTabs: true }); // 'hello\tworld'
removeNonPrintable("hello\r\nworld", { keepCarriageReturns: true }); // 'hello\rworld'

// Preserves emoji with zero-width joiners
removeNonPrintable("👨‍👩‍👧‍👦"); // '👨‍👩‍👧‍👦' (family emoji preserved)
removeNonPrintable("text\x1B[32mgreen\x1B[0m"); // 'text[32mgreen[0m' (ANSI escapes removed)

highlight(str: string, terms: string | string[], options?: HighlightOptions): string

Highlights search terms in text by wrapping them with markers.

highlight("The quick brown fox", "quick"); // 'The <mark>quick</mark> brown fox'
highlight("Hello WORLD", "world"); // '<mark>Hello</mark> <mark>WORLD</mark>' (case-insensitive by default)

// Multiple terms
highlight("The quick brown fox", ["quick", "fox"]); // 'The <mark>quick</mark> brown <mark>fox</mark>'

// Custom wrapper
highlight("Error: Connection failed", ["error", "failed"], {
  wrapper: ["**", "**"],
}); // '**Error**: Connection **failed**'

// Whole word matching
highlight("Java and JavaScript", "Java", { wholeWord: true }); // '<mark>Java</mark> and JavaScript'

// With CSS class
highlight("Hello world", "Hello", { className: "highlight" }); // '<mark class="highlight">Hello</mark> world'

// HTML escaping for security
highlight("<div>Hello</div>", "Hello", { escapeHtml: true }); // '&lt;div&gt;<mark>Hello</mark>&lt;/div&gt;'

// Case-sensitive matching
highlight("Hello hello", "hello", { caseSensitive: true }); // 'Hello <mark>hello</mark>'

Options:

  • caseSensitive - Enable case-sensitive matching (default: false)
  • wholeWord - Match whole words only (default: false)
  • wrapper - Custom wrapper tags (default: ['', ''])
  • className - CSS class for mark tags
  • escapeHtml - Escape HTML in text before highlighting (default: false)

diff(oldStr: string, newStr: string): string

Computes a simple string diff comparison showing additions and deletions.

diff("hello world", "hello beautiful world"); // 'hello {+beautiful +}world'
diff("goodbye world", "hello world"); // '[-goodbye-]{+hello+} world'
diff("v1.0.0", "v1.1.0"); // 'v1.[-0-]{+1+}.0'
diff("debug: false", "debug: true"); // 'debug: [-fals-]{+tru+}e'
diff("user@example.com", "admin@example.com"); // '[-user-]{+admin+}@example.com'

// Form field changes
diff("John Doe", "Jane Doe"); // 'J[-ohn-]{+ane+} Doe'

// Configuration changes
diff("port: 3000", "port: 8080"); // 'port: [-300-]{+808+}0'

// File extension changes
diff("app.js", "app.ts"); // 'app.[-j-]{+t+}s'

// No changes
diff("test", "test"); // 'test'

// Complete replacement
diff("hello", "world"); // '[-hello-]{+world+}'

Uses a simple prefix/suffix algorithm optimized for readability. The output format uses:

  • [-text-] for deleted text
  • {+text+} for added text

levenshtein(a: string, b: string, maxDistance?: number): number

Calculates the Levenshtein distance (edit distance) between two strings. Optimized with space-efficient algorithm and early termination support.

levenshtein("cat", "bat"); // 1 (substitution)
levenshtein("cat", "cats"); // 1 (insertion)
levenshtein("cats", "cat"); // 1 (deletion)
levenshtein("kitten", "sitting"); // 3
levenshtein("example", "exmaple"); // 2 (transposition)

// With maxDistance for early termination
levenshtein("hello", "helicopter", 3); // Infinity (exceeds max)
levenshtein("hello", "hallo", 3); // 1 (within max)

// Unicode support
levenshtein("café", "cafe"); // 1
levenshtein("😀", "😃"); // 1

levenshteinNormalized(a: string, b: string): number

Calculates normalized Levenshtein similarity score between 0 and 1. Perfect for fuzzy matching and similarity scoring.

levenshteinNormalized("hello", "hello"); // 1 (identical)
levenshteinNormalized("cat", "bat"); // 0.667 (fairly similar)
levenshteinNormalized("hello", "world"); // 0.2 (dissimilar)
levenshteinNormalized("", "abc"); // 0 (completely different)

// Real-world typo detection
levenshteinNormalized("necessary", "neccessary"); // 0.9
levenshteinNormalized("example", "exmaple"); // 0.714

// Fuzzy matching (common threshold: 0.8)
const threshold = 0.8;
levenshteinNormalized("test", "tests") >= threshold; // true (0.8)
levenshteinNormalized("hello", "goodbye") >= threshold; // false (0.143)

fuzzyMatch(query: string, target: string, options?: FuzzyMatchOptions): FuzzyMatchResult | null

Performs fuzzy string matching with a similarity score, ideal for command palettes, file finders, and search-as-you-type features.

// Basic usage
fuzzyMatch("gto", "goToLine"); // { matched: true, score: 0.546 }
fuzzyMatch("usrctrl", "userController.js"); // { matched: true, score: 0.444 }
fuzzyMatch("abc", "xyz"); // null (no match)

// Command palette style matching
fuzzyMatch("of", "openFile"); // { matched: true, score: 0.75 }
fuzzyMatch("svf", "saveFile"); // { matched: true, score: 0.619 }

// File finder matching
fuzzyMatch("index", "src/components/index.html"); // { matched: true, score: 0.262 }
fuzzyMatch("app.js", "src/app.js"); // { matched: true, score: 0.85 }

// Case sensitivity
fuzzyMatch("ABC", "abc"); // { matched: true, score: 0.95 }
fuzzyMatch("ABC", "abc", { caseSensitive: true }); // null

// Minimum score threshold
fuzzyMatch("ab", "a" + "x".repeat(50) + "b", { threshold: 0.5 }); // null (score too low)

// Acronym matching (matches at word boundaries score higher)
fuzzyMatch("uc", "UserController"); // { matched: true, score: 0.75 }
fuzzyMatch("gc", "getUserController"); // { matched: true, score: 0.75 }

Options:

  • caseSensitive - Enable case-sensitive matching (default: false)
  • threshold - Minimum score to consider a match (default: 0)

Returns:

  • { matched: true, score: number } - When match found (score between 0-1)
  • { matched: false, score: 0 } - For empty query
  • null - When no match found or score below threshold

Scoring algorithm prioritizes:

  • Exact matches (1.0)
  • Prefix matches (≥0.85)
  • Consecutive character matches
  • Matches at word boundaries (camelCase, snake_case, kebab-case, etc.)
  • Early matches in the string
  • Acronym-style matches

pad(str: string, length: number, chars?: string): string

Pads a string to a given length by adding characters to both sides (centers the string).

pad("Hi", 6); // '  Hi  '
pad("Hi", 6, "-"); // '--Hi--'
pad("Hi", 7, "-"); // '--Hi---'

padStart(str: string, length: number, chars?: string): string

Pads a string to a given length by adding characters to the left.

padStart("5", 3, "0"); // '005'
padStart("Hi", 5, "."); // '...Hi'
padStart("Hi", 6, "=-"); // '=-=-Hi'

padEnd(str: string, length: number, chars?: string): string

Pads a string to a given length by adding characters to the right.

padEnd("Hi", 5, "."); // 'Hi...'
padEnd("Hi", 6, "=-"); // 'Hi=-=-'
padEnd("5", 3, "0"); // '500'

graphemes(str: string): string[]

Splits a string into an array of grapheme clusters, properly handling emojis, combining characters, and complex Unicode.

graphemes("hello"); // ['h', 'e', 'l', 'l', 'o']
graphemes("👨‍👩‍👧‍👦🎈"); // ['👨‍👩‍👧‍👦', '🎈']
graphemes("café"); // ['c', 'a', 'f', 'é']
graphemes("👍🏽"); // ['👍🏽'] - emoji with skin tone
graphemes("🇺🇸"); // ['🇺🇸'] - flag emoji
graphemes("hello👋world"); // ['h', 'e', 'l', 'l', 'o', '👋', 'w', 'o', 'r', 'l', 'd']

codePoints(str: string): number[]

Converts a string into an array of Unicode code points, properly handling surrogate pairs and complex characters.

codePoints("hello"); // [104, 101, 108, 108, 111]
codePoints("👍"); // [128077]
codePoints("€"); // [8364]
codePoints("Hello 👋"); // [72, 101, 108, 108, 111, 32, 128075]
codePoints("a👍b"); // [97, 128077, 98]
codePoints("👨‍👩‍👧‍👦"); // [128104, 8205, 128105, 8205, 128103, 8205, 128102]

Performance Utilities

memoize<T>(fn: T, options?: MemoizeOptions): T

Creates a memoized version of a function with LRU (Least Recently Used) cache eviction. Ideal for optimizing expensive string operations like levenshtein, fuzzyMatch, or diff when processing repetitive data.

import { levenshtein, memoize } from "nano-string-utils";

// Basic usage - memoize expensive string operations
const memoizedLevenshtein = memoize(levenshtein);

// First call computes the result
memoizedLevenshtein("kitten", "sitting"); // 3 (computed)

// Subsequent calls with same arguments return cached result
memoizedLevenshtein("kitten", "sitting"); // 3 (cached - instant)

// Custom cache size (default is 100)
const limited = memoize(levenshtein, { maxSize: 50 });

// Custom key generation for complex arguments
const processUser = (user) => expensive(user);
const memoizedProcess = memoize(processUser, {
  getKey: (user) => user.id, // Cache by user ID only
});

// Real-world example: Fuzzy search with caching
import { fuzzyMatch, memoize } from "nano-string-utils";

const cachedFuzzyMatch = memoize(fuzzyMatch);
const searchResults = items.map((item) => cachedFuzzyMatch(query, item.name));

// Batch processing with deduplication benefits
const words = ["hello", "world", "hello", "test", "world"];
const distances = words.map((word) => memoizedLevenshtein("example", word)); // Only computes 3 times instead of 5

Features:

  • LRU cache eviction - Keeps most recently used results
  • Configurable cache size - Control memory usage (default: 100 entries)
  • Custom key generation - Support for complex argument types
  • Type-safe - Preserves function signatures and types
  • Zero dependencies - Pure JavaScript implementation

Best used with:

  • levenshtein() - Expensive O(n×m) algorithm
  • fuzzyMatch() - Complex scoring with boundary detection
  • diff() - Character-by-character comparison
  • Any custom expensive string operations

Options:

  • maxSize - Maximum cached results (default: 100)
  • getKey - Custom cache key generator function

String Generation

randomString(length: number, charset?: string): string

Generates a random string of specified length.

randomString(10); // 'aBc123XyZ9'
randomString(5, "abc"); // 'abcab'
randomString(8, "0123456789"); // '42318765'

hashString(str: string): number

Generates a simple hash from a string (non-cryptographic).

hashString("hello"); // 99162322
hashString("world"); // 113318802

String Validation

isEmail(str: string): boolean

Validates if a string is a valid email format.

isEmail("user@example.com"); // true
isEmail("invalid.email"); // false
isEmail("test@sub.domain.com"); // true

isUrl(str: string): boolean

Validates if a string is a valid URL format.

isUrl("https://example.com"); // true
isUrl("http://localhost:3000"); // true
isUrl("not a url"); // false
isUrl("ftp://files.com/file.zip"); // true

isASCII(str: string): boolean

Checks if a string contains only ASCII characters (code points 0-127).

isASCII("Hello World!"); // true
isASCII("café"); // false
isASCII("👍"); // false
isASCII("abc123!@#"); // true
isASCII(""); // true

toASCII(str: string, options?: { placeholder?: string }): string

Converts a string to ASCII-safe representation by removing diacritics, converting common Unicode symbols, and optionally replacing non-ASCII characters.

toASCII("café"); // 'cafe'
toASCII("Hello "world""); // 'Hello "world"'
toASCII("em—dash"); // 'em-dash'
toASCII("€100"); // 'EUR100'
toASCII("½ + ¼ = ¾"); // '1/2 + 1/4 = 3/4'
toASCII("→ ← ↑ ↓"); // '-> <- ^ v'
toASCII("α β γ"); // 'a b g'
toASCII("Привет"); // 'Privet'
toASCII("你好"); // '' (removes non-convertible characters)
toASCII("你好", { placeholder: "?" }); // '??'
toASCII("Hello 世界", { placeholder: "?" }); // 'Hello ??'
toASCII("© 2024 Müller™"); // '(c) 2024 Muller(TM)'

Features:

  • Removes diacritics/accents (café → cafe)
  • Converts smart quotes to regular quotes
  • Converts Unicode dashes to hyphens
  • Converts mathematical symbols (≈ → ~, ≠ → !=)
  • Converts currency symbols (€ → EUR, £ → GBP)
  • Converts fractions (½ → 1/2)
  • Transliterates common Greek and Cyrillic letters
  • Handles emojis and multi-byte Unicode correctly
  • Optional placeholder for non-convertible characters

pluralize(word: string, count?: number): string

Converts a singular word to its plural form using English pluralization rules. Optionally takes a count to conditionally pluralize.

pluralize("box"); // 'boxes'
pluralize("baby"); // 'babies'
pluralize("person"); // 'people'
pluralize("analysis"); // 'analyses'
pluralize("cactus"); // 'cacti'

// With count parameter
pluralize("item", 1); // 'item' (singular for count of 1)
pluralize("item", 0); // 'items' (plural for count of 0)
pluralize("item", 5); // 'items' (plural for count > 1)

// Preserves casing
pluralize("Box"); // 'Boxes'
pluralize("PERSON"); // 'PEOPLE'

Features:

  • Handles common irregular plurals (person→people, child→children, etc.)
  • Supports standard rules (s/es, y→ies, f→ves)
  • Handles Latin/Greek patterns (analysis→analyses, datum→data, cactus→cacti)
  • Preserves original casing
  • Optional count parameter for conditional pluralization

singularize(word: string): string

Converts a plural word to its singular form using English singularization rules.

singularize("boxes"); // 'box'
singularize("babies"); // 'baby'
singularize("people"); // 'person'
singularize("analyses"); // 'analysis'
singularize("cacti"); // 'cactus'
singularize("data"); // 'datum'

// Preserves casing
singularize("Boxes"); // 'Box'
singularize("PEOPLE"); // 'PERSON'

Features:

  • Reverses common pluralization patterns
  • Handles irregular plural mappings
  • Supports Latin/Greek plural forms
  • Preserves original casing
  • Handles edge cases like unchanged plurals (sheep→sheep)

Branded Types (TypeScript)

Nano-string-utils provides branded types for compile-time type safety with validated strings. These types add zero runtime overhead and are fully tree-shakeable.

import { branded } from "nano-string-utils";

Type Guards

Type guards narrow string types to branded types:

const input: string = getUserInput();

if (branded.isValidEmail(input)) {
  // input is now typed as Email
  sendEmail(input);
}

if (branded.isValidUrl(input)) {
  // input is now typed as URL
  fetch(input);
}

if (branded.isSlug(input)) {
  // input is now typed as Slug
  useAsRoute(input);
}

Builder Functions

Safely create branded types with validation:

// Returns Email | null
const email = branded.toEmail("user@example.com");
if (email) {
  sendEmail(email); // email is typed as Email
}

// Returns URL | null
const url = branded.toUrl("https://example.com");
if (url) {
  fetch(url); // url is typed as URL
}

// Always returns Slug (transforms input)
const slug = branded.toSlug("Hello World!"); // 'hello-world' as Slug
createRoute(slug);

// Smart slug handling
const slug2 = branded.ensureSlug("already-a-slug"); // returns as-is if valid
const slug3 = branded.ensureSlug("Not A Slug!"); // transforms to 'not-a-slug'

Assertion Functions

Assert types with runtime validation:

const input: string = getUserInput();

// Throws BrandedTypeError if invalid
branded.assertEmail(input);
// input is now typed as Email
sendEmail(input);

// Custom error messages
branded.assertUrl(input, "Invalid webhook URL");

// All assertion functions available
branded.assertEmail(str);
branded.assertUrl(str);
branded.assertSlug(str);

Unsafe Variants

For trusted inputs where validation isn't needed:

// Use only when you're certain the input is valid
const trustedEmail = branded.unsafeEmail("admin@system.local");
const trustedUrl = branded.unsafeUrl("https://internal.api");
const trustedSlug = branded.unsafeSlug("already-valid-slug");

Available Types

  • Email - Validated email addresses
  • URL - Validated URLs (http/https/ftp/ftps)
  • Slug - URL-safe slugs (lowercase, hyphenated)
  • Brand<T, K> - Generic branding utility for custom types

Benefits

  • Zero runtime overhead - Types are erased at compilation
  • Type safety - Prevent passing unvalidated strings to functions
  • IntelliSense support - Full autocomplete and type hints
  • Tree-shakeable - Only imported if used
  • Composable - Works with existing string functions
// Example: Type-safe API
function sendNewsletter(email: branded.Email) {
  // Can only be called with validated emails
  api.send(email);
}

// Won't compile without validation
const userInput = "maybe@email.com";
// sendNewsletter(userInput); // ❌ Type error!

// Must validate first
const validated = branded.toEmail(userInput);
if (validated) {
  sendNewsletter(validated); // ✅ Type safe!
}

Template Literal Types (TypeScript)

Case conversion functions now provide precise type inference for literal strings at compile time. This feature enhances IDE support with exact type transformations while maintaining full backward compatibility.

import { camelCase, kebabCase, snakeCase } from "nano-string-utils";

// Literal strings get exact transformed types
const endpoint = kebabCase("getUserProfile");
// Type: "get-user-profile" (not just string!)

const column = snakeCase("firstName");
// Type: "first_name"

const methodName = camelCase("fetch-user-data");
// Type: "fetchUserData"

// Runtime strings still return regular string type
const userInput: string = getUserInput();
const result = camelCase(userInput);
// Type: string (backward compatible)

All Case Conversions Support Template Literals

camelCase("hello-world"); // Type: "helloWorld"
kebabCase("helloWorld"); // Type: "hello-world"
snakeCase("HelloWorld"); // Type: "hello_world"
pascalCase("hello-world"); // Type: "HelloWorld"
constantCase("helloWorld"); // Type: "HELLO_WORLD"
dotCase("HelloWorld"); // Type: "hello.world"
pathCase("helloWorld"); // Type: "hello/world"
sentenceCase("hello-world"); // Type: "Hello world"
titleCase("hello-world"); // Type: "Hello World"

Type-Safe Configuration Objects

Transform configuration keys between naming conventions:

const config = {
  "api-base-url": "https://api.example.com",
  "max-retries": 3,
} as const;

// Convert keys to camelCase at type level
type ConfigCamelCase = {
  [K in keyof typeof config as CamelCase<K>]: (typeof config)[K];
};
// Type: { apiBaseUrl: string; maxRetries: number; }

API Route Mapping

Create type-safe method names from API routes:

type ApiRoutes = "user-profile" | "user-settings" | "admin-panel";

type MethodNames = {
  [K in ApiRoutes as `fetch${PascalCase<K>}`]: () => Promise<void>;
};
// Creates: fetchUserProfile(), fetchUserSettings(), fetchAdminPanel()

Benefits:

  • Zero runtime cost - All transformations happen at compile time
  • Better IDE support - Autocomplete shows exact transformed strings
  • Type safety - Catch typos and incorrect transformations during development
  • Backward compatible - Runtime strings work exactly as before

Null/Undefined Safety

All functions in nano-string-utils handle null and undefined inputs gracefully:

// No more runtime errors!
slugify(null); // Returns: null
slugify(undefined); // Returns: undefined
slugify(""); // Returns: ""

// Consistent behavior across all functions
isEmail(null); // Returns: false (validation functions)
words(null); // Returns: [] (array functions)
wordCount(null); // Returns: 0 (counting functions)

// Safe to use without defensive checks
const userInput = getUserInput(); // might be null/undefined
const slug = slugify(userInput); // Won't throw!

This means:

  • No TypeErrors - Functions never throw on null/undefined
  • Predictable behavior - Consistent handling across all utilities
  • Cleaner code - No need for defensive checks before calling functions
  • Zero performance cost - Minimal overhead from null checks

Bundle Size

Each utility is optimized to be as small as possible:

Function Size (minified)
slugify ~200 bytes
camelCase ~250 bytes
snakeCase ~220 bytes
kebabCase ~200 bytes
pascalCase ~180 bytes
constantCase ~230 bytes
dotCase ~210 bytes
pathCase ~210 bytes
sentenceCase ~280 bytes
titleCase ~320 bytes
capitalize ~100 bytes
truncate ~150 bytes
stripHtml ~120 bytes
escapeHtml ~180 bytes
excerpt ~220 bytes
randomString ~200 bytes
hashString ~150 bytes
reverse ~80 bytes
deburr ~200 bytes
isEmail ~180 bytes
isUrl ~200 bytes
isASCII ~100 bytes
toASCII ~450 bytes
wordCount ~100 bytes
normalizeWhitespace ~280 bytes
removeNonPrintable ~200 bytes
template ~350 bytes
templateSafe ~400 bytes
pad ~180 bytes
padStart ~150 bytes
padEnd ~150 bytes
graphemes ~250 bytes
codePoints ~120 bytes
highlight ~320 bytes
diff ~280 bytes
levenshtein ~380 bytes
levenshteinNormalized ~100 bytes
fuzzyMatch ~500 bytes
pluralize ~350 bytes
singularize ~320 bytes
memoize ~400 bytes

Total package size: < 6.5KB minified + gzipped

Requirements

  • Node.js >= 18
  • TypeScript >= 5.0 (for TypeScript users)

Development

# Clone the repository
git clone https://github.com/Zheruel/nano-string-utils.git
cd nano-string-utils

# Install dependencies
npm install

# Run tests
npm test

# Run tests with coverage
npm run test:coverage

# Build the library
npm run build

# Type check
npm run typecheck

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

MIT © [Zheruel]


Why nano-string-utils?

In a world of bloated dependencies, nano-string-utils stands out by providing exactly what you need and nothing more:

  • Security First: Zero dependencies means zero supply chain vulnerabilities
  • Performance: Optimized for both speed and size
    • 2.1-2.6x faster than lodash for truncate
    • 25x faster than lodash for template
    • 2.4x faster than lodash for capitalize
  • Developer Experience: Full TypeScript support with comprehensive JSDoc comments
  • Production Ready: 100% test coverage with extensive edge case handling
  • Modern: Built for ES2022+ with full ESM support and CommonJS compatibility

Benchmarks

We continuously benchmark nano-string-utils against popular alternatives (lodash and es-toolkit) to ensure optimal performance and bundle size.

Running Benchmarks

# Run all benchmarks
npm run bench:all

# Run performance benchmarks only
npm run bench:perf

# Run bundle size analysis only
npm run bench:size

Latest Results

Bundle Size Comparison (gzipped)

Function nano-string-utils lodash es-toolkit Winner
camelCase 193B 3.4KB 269B nano ✅
capitalize 90B 1.7KB 99B nano ✅
kebabCase 161B 2.8KB 193B nano ✅
truncate 125B 2.9KB - nano ✅

View full benchmark results

Key Findings

  • 🏆 Smallest bundle sizes: nano-string-utils wins 10 out of 11 tested functions
  • Superior performance: 2-25x faster than lodash for key operations
  • 📊 Detailed benchmarks: See benchmark-results.md for full comparison
  • Optimized performance:
    • 2.25x faster than lodash for short string truncation
    • Case conversions improved by 30-40% in latest optimizations
    • Truncate function improved by 97.6% (42x faster!)
  • 🌳 Superior tree-shaking: Each function is independently importable with minimal overhead

Comparison with Alternatives

Library Bundle Size Dependencies Tree-shakeable TypeScript
nano-string-utils < 6.5KB 0
lodash ~70KB 0 ⚠️ Requires lodash-es
underscore.string ~20KB 0
voca ~30KB 0

Support

Package Sidebar

Install

npm i nano-string-utils

Weekly Downloads

721

Version

0.9.1

License

MIT

Unpacked Size

524 kB

Total Files

10

Last publish

Collaborators

  • zheruel