@repo/utils
Common utility functions for string manipulation, validation, formatting, and async operations. Merge Tailwind classes
with cn(), format currency, validate emails, debounce functions. Tree-shakeable for small bundles.
Quick Start
Add utilities in 2 minutes:
pnpm add @repo/utilsTree-shakeable, tested, ready to use. Skip to Quick Start →
Why @repo/utils?
Every package reimplements debounce differently. String utilities like truncate are duplicated across apps. Email
validation regex differs by project (some accept invalid emails). Currency formatting is inconsistent. Tailwind class
merging needs special handling (later classes override earlier ones). Date formatting uses different libraries.
@repo/utils solves this with shared, tested utilities for common tasks.
Production-ready with tree-shaking support, TypeScript types, comprehensive tests, and zero dependencies for core utilities.
Use cases
- Tailwind class merging — Merge and override Tailwind classes correctly with
cn() - String formatting — Truncate, slugify, capitalize, template strings
- Date/currency formatting — Format dates, currency, numbers with locale support
- Email/URL validation — Validate user input with regex patterns
- Debounce/throttle — Performance optimization for search, scroll, resize
How it works
@repo/utils exports tree-shakeable utility functions:
import { cn, debounce, formatCurrency, isValidEmail } from "@repo/utils";
// Merge Tailwind classes (later classes override earlier)
const classes = cn("px-4 py-2", isActive && "bg-blue-500", isDisabled && "opacity-50");
// Debounce search input
const debouncedSearch = debounce((query: string) => {
// API call
}, 300);
// Format currency with locale
formatCurrency(99.99, { currency: "USD" }); // "$99.99"
// Validate email
isValidEmail("user@example.com"); // trueUses clsx for class merging, Intl API for formatting, tested regex for validation, and optimized async utilities.
Key features
Tailwind class merging — cn() utility handles conditional classes and overrides
String utilities — Truncate, slugify, capitalize, template strings
Date formatting — Format dates, relative time ("2 hours ago"), date math
Number/currency formatting — Locale-aware formatting with Intl API
Validation — Email, URL, empty checks with type guards
Async utilities — Debounce, throttle, sleep, retry with exponential backoff
Quick Start
1. Install the package
pnpm add @repo/utils2. Merge Tailwind classes with cn()
import { cn } from "@repo/utils";
interface ButtonProps {
variant?: "primary" | "secondary";
disabled?: boolean;
className?: string;
}
export function Button({ variant = "primary", disabled, className }: ButtonProps) {
return (
<button
className={cn(
"px-4 py-2 rounded-md font-medium",
variant === "primary" && "bg-blue-500 text-white",
variant === "secondary" && "bg-gray-200 text-gray-900",
disabled && "opacity-50 cursor-not-allowed",
className // User classes override defaults
)}
disabled={disabled}
>
Click me
</button>
);
}3. Format dates and currency
import { formatCurrency, formatDate, relativeTime } from "@repo/utils";
export function ProductCard({ product }: { product: Product }) {
return (
<div>
<h3>{product.name}</h3>
<p className="text-2xl font-bold">
{formatCurrency(product.price, { currency: "USD" })}
</p>
<p className="text-sm text-gray-600">
Listed {relativeTime(product.createdAt)}
</p>
);
}4. Validate user input
"use client";
import { useState } from "react";
import { isValidEmail } from "@repo/utils";
export function NewsletterForm() {
const [email, setEmail] = useState("");
const [error, setError] = useState("");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!isValidEmail(email)) {
setError("Please enter a valid email address");
return;
}
// Submit email
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
/>
{error && <p className="text-red-500">{error}</p>}
<button type="submit">Subscribe</button>
</form>
);
}That's it! You now have utilities for class merging, formatting, and validation across your app.
Debounce search and scroll handlers
Optimize performance with debounce and throttle:
import { debounce, throttle } from "@repo/utils";
// Debounce search (wait for user to stop typing)
const debouncedSearch = debounce((query: string) => {
fetch(`/api/search?q=${query}`);
}, 300);
// Throttle scroll handler (run at most every 100ms)
const throttledScroll = throttle(() => {
updateScrollPosition();
}, 100);Technical Details
For Developers: Technical implementation details
Overview
| Property | Value |
|---|---|
| Location | packages/utils |
| Purpose | Common utilities, helpers |
Export Paths
| Path | Description |
|---|---|
@repo/utils | All utilities |
@repo/utils/string | String utilities |
@repo/utils/array | Array utilities |
@repo/utils/object | Object utilities |
@repo/utils/date | Date utilities |
@repo/utils/validation | Validation helpers |
Class Name Utility
Tailwind Integration
The cn utility is optimized for Tailwind CSS and handles class merging correctly.
import { cn } from "@repo/utils";
// Merge class names
cn("px-4 py-2", "bg-blue-500");
// => "px-4 py-2 bg-blue-500"
// highlight-start
// Conditional classes
cn("base-class", isActive && "active", isDisabled && "disabled");
// => "base-class active" (if isActive is true)
// highlight-end
// Object syntax
cn({
"text-red-500": hasError,
"text-green-500": isSuccess
});String Utilities
Truncate
import { truncate } from "@repo/utils/string";
truncate("This is a long string", 10);
// => "This is a..."
truncate("Short", 10);
// => "Short"Slugify
import { slugify } from "@repo/utils/string";
slugify("Hello World!");
// => "hello-world"
slugify("Café & Restaurant");
// => "cafe-restaurant"Capitalize
import { capitalize, capitalizeWords } from "@repo/utils/string";
capitalize("hello");
// => "Hello"
capitalizeWords("hello world");
// => "Hello World"Template
import { template } from "@repo/utils/string";
template("Hello, {name}!", { name: "World" });
// => "Hello, World!"Array Utilities
Chunk
import { chunk } from "@repo/utils/array";
chunk([1, 2, 3, 4, 5], 2);
// => [[1, 2], [3, 4], [5]]Unique
import { unique, uniqueBy } from "@repo/utils/array";
unique([1, 2, 2, 3, 3, 3]);
// => [1, 2, 3]
uniqueBy(users, (u) => u.email);
// => unique users by emailGroup By
import { groupBy } from "@repo/utils/array";
const posts = [
{ category: "tech", title: "Post 1" },
{ category: "life", title: "Post 2" },
{ category: "tech", title: "Post 3" }
];
groupBy(posts, (p) => p.category);
// => { tech: [...], life: [...] }Shuffle
import { shuffle } from "@repo/utils/array";
shuffle([1, 2, 3, 4, 5]);
// => [3, 1, 5, 2, 4] (random order)Object Utilities
Pick
import { pick } from "@repo/utils/object";
const user = { id: 1, name: "John", email: "john@example.com", password: "..." };
pick(user, ["id", "name"]);
// => { id: 1, name: "John" }Omit
import { omit } from "@repo/utils/object";
omit(user, ["password"]);
// => { id: 1, name: "John", email: "john@example.com" }Deep Clone
import { deepClone } from "@repo/utils/object";
const cloned = deepClone(complexObject);Deep Merge
import { deepMerge } from "@repo/utils/object";
const merged = deepMerge(defaultConfig, userConfig);Date Utilities
Format Date
import { formatDate } from "@repo/utils/date";
formatDate(new Date());
// => "January 15, 2024"
formatDate(new Date(), { format: "short" });
// => "Jan 15, 2024"
formatDate(new Date(), { format: "iso" });
// => "2024-01-15"Relative Time
import { relativeTime } from "@repo/utils/date";
relativeTime(new Date(Date.now() - 3600000));
// => "1 hour ago"
relativeTime(new Date(Date.now() + 86400000));
// => "in 1 day"Date Math
import { addDays, addMonths, startOfDay, endOfDay } from "@repo/utils/date";
addDays(new Date(), 7);
// => Date 7 days from now
startOfDay(new Date());
// => Date at 00:00:00
endOfDay(new Date());
// => Date at 23:59:59Formatting
Format Currency
import { formatCurrency } from "@repo/utils";
formatCurrency(99.99);
// => "$99.99"
formatCurrency(99.99, { currency: "EUR", locale: "de-DE" });
// => "99,99 €"Format Number
import { formatNumber } from "@repo/utils";
formatNumber(1234567);
// => "1,234,567"
formatNumber(1234567, { notation: "compact" });
// => "1.2M"Format Bytes
import { formatBytes } from "@repo/utils";
formatBytes(1024);
// => "1 KB"
formatBytes(1536000);
// => "1.46 MB"Validation
Input Validation
Always validate user input on both client and server. These utilities help ensure data integrity.
import { isValidEmail } from "@repo/utils/validation";
// highlight-start
isValidEmail("user@example.com"); // true
isValidEmail("invalid-email"); // false
// highlight-endURL
import { isValidUrl } from "@repo/utils/validation";
isValidUrl("https://example.com"); // true
isValidUrl("not-a-url"); // falseEmpty Check
import { isEmpty, isNotEmpty } from "@repo/utils/validation";
isEmpty(""); // true
isEmpty([]); // true
isEmpty({}); // true
isEmpty(null); // true
isNotEmpty("hello"); // true
isNotEmpty([1, 2]); // trueAsync Utilities
Debounce
import { debounce } from "@repo/utils";
const debouncedSearch = debounce((query: string) => {
// Search logic
}, 300);
// Called multiple times, only executes after 300ms of no calls
debouncedSearch("hello");Throttle
import { throttle } from "@repo/utils";
const throttledScroll = throttle(() => {
// Scroll handler
}, 100);
// Called at most once every 100msSleep
import { sleep } from "@repo/utils";
await sleep(1000); // Wait 1 secondRetry
import { retry } from "@repo/utils";
const result = await retry(() => fetchData(), { maxRetries: 3, delay: 1000 });Related Packages
- @repo/types - TypeScript types
- @repo/core-utils - Additional utilities