TypeScript Utility Library
A curated collection of type-safe utility functions, custom React hooks, and advanced TypeScript patterns that eliminate frontend boilerplate. Includes 50+ utilities: type-safe event emitters, deep object manipulation, runtime type guards, branded types, discriminated unions, debounce/throttle, and custom hooks for localStorage, media queries, and keyboard shortcuts. Full JSDoc, comprehensive tests, zero dependencies.
Key Features
- Type-Safe Utilities — Object manipulation, array helpers, string formatting, and date functions with strict TypeScript types
- Custom React Hooks — 20+ hooks for localStorage, media queries, intersection observer, keyboard shortcuts, and clipboard
- Advanced Generic Patterns — Builder pattern, result types, branded types, and type-safe event emitters with full inference
-
Runtime Type Guards —
is*functions for narrowing unknown data at API boundaries - Async Utilities — Debounce, throttle, retry with backoff, and cancellable fetch wrappers
- Full Test Coverage — Every utility includes Vitest tests covering edge cases, error paths, and type assertions
- Tree-Shakeable — Each utility is independently importable; bundlers include only what you use
Quick Start
Copy the
utils/directory into your project'ssrc/lib/folder.Import individual utilities:
import { debounce } from '@/lib/utils/async';
import { groupBy, uniqueBy } from '@/lib/utils/array';
import { useLocalStorage } from '@/lib/utils/hooks';
import { isNonNullable } from '@/lib/utils/guards';
- Use with full type inference:
function SearchPage() {
const [query, setQuery] = useLocalStorage('search-query', '');
const handleSearch = debounce((value: string) => {
// value is typed as string, not unknown
fetchResults(value);
}, 300);
return <input value={query} onChange={(e) => { setQuery(e.target.value); handleSearch(e.target.value); }} />;
}
Architecture / How It Works
typescript-utility-library/
├── utils/ # array, object, string, number, date, async, guards, result
├── hooks/ # useLocalStorage, useMediaQuery, useDebounce, useIntersection,
│ # useKeyboardShortcut, useClipboard, useEventListener, usePrevious
├── types/ # branded.ts, unions.ts, paths.ts, utility.ts
└── tests/ # Vitest tests for all utilities and hooks
Usage Examples
Type-Safe groupBy
function groupBy<T, K extends string>(items: T[], keyFn: (item: T) => K): Record<K, T[]> {
return items.reduce((acc, item) => {
const key = keyFn(item);
return { ...acc, [key]: [...(acc[key] ?? []), item] };
}, {} as Record<K, T[]>);
}
// Usage — return type is Record<'active' | 'inactive', User[]>
const grouped = groupBy(users, (u) => u.status);
grouped.active; // User[] — fully typed
Result Type for Error Handling
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
function ok<T>(value: T): Result<T, never> { return { ok: true, value }; }
function err<E>(error: E): Result<never, E> { return { ok: false, error }; }
async function fetchUser(id: string): Promise<Result<User, 'not_found' | 'network_error'>> {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) return err('not_found');
return ok(await res.json());
} catch { return err('network_error'); }
}
const result = await fetchUser('123');
if (result.ok) console.log(result.value.name); // User is typed
else console.log(result.error); // 'not_found' | 'network_error'
Branded Types for Domain Safety
declare const brand: unique symbol;
type Brand<T, B> = T & { readonly [brand]: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
function createUserId(id: string): UserId { return id as UserId; }
function createOrderId(id: string): OrderId { return id as OrderId; }
function getUser(id: UserId): Promise<User> { /* ... */ }
const userId = createUserId('usr_123');
const orderId = createOrderId('ord_456');
getUser(userId); // OK
getUser(orderId); // TypeScript ERROR — OrderId not assignable to UserId
Custom Hook: useLocalStorage
export function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue;
try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; }
catch { return initialValue; }
});
const set = useCallback((v: T | ((prev: T) => T)) => {
setValue((prev) => {
const next = v instanceof Function ? v(prev) : v;
window.localStorage.setItem(key, JSON.stringify(next));
return next;
});
}, [key]);
useEffect(() => {
const handler = (e: StorageEvent) => { if (e.key === key && e.newValue) setValue(JSON.parse(e.newValue)); };
window.addEventListener('storage', handler);
return () => window.removeEventListener('storage', handler);
}, [key]);
return [value, set] as const;
}
Configuration
Strict TypeScript Config
{
"compilerOptions": {
"target": "ES2022", "lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext", "moduleResolution": "bundler",
"strict": true, "noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true, "noImplicitReturns": true,
"jsx": "react-jsx",
"paths": { "@/lib/*": ["./src/lib/*"] }
}
}
Best Practices
-
Use
noUncheckedIndexedAccess— forces handlingundefinedwhen accessing arrays/objects by index -
Prefer
unknownoverany— use type guards to narrow instead of bypassing the type system -
Use branded types for IDs — prevents mixing
UserIdandOrderIdat compile time -
Return
Result<T, E>— makes error handling explicit; callers can't forget to check -
Cleanup side effects in hooks — every
addEventListenerorsubscribemust return a cleanup function
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
useLocalStorage causes hydration mismatch |
Server renders initial value, client reads stored value | Use useEffect for initial read or wrap in Suspense with client-only fallback |
| Generic function loses type inference | Explicit type annotation overrides inference | Remove explicit generics and let TypeScript infer from arguments |
exactOptionalPropertyTypes breaks existing code |
undefined must be explicitly handled |
Use `field?: T \ |
| Debounce callback has stale closure | Callback captures variables from old render | Use {% raw %}useRef to hold the latest callback or use the included useLatestCallback hook |
This is 1 of 11 resources in the Frontend Developer Pro toolkit. Get the complete [TypeScript Utility Library] with all files, templates, and documentation for $29.
Or grab the entire Frontend Developer Pro bundle (11 products) for $129 — save 30%.
Top comments (0)