DEV Community

Thesius Code
Thesius Code

Posted on • Originally published at datanest-stores.pages.dev

TypeScript Utility Library

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 Guardsis* 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

  1. Copy the utils/ directory into your project's src/lib/ folder.

  2. 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';
Enter fullscreen mode Exit fullscreen mode
  1. 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); }} />;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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/*"] }
  }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

  • Use noUncheckedIndexedAccess — forces handling undefined when accessing arrays/objects by index
  • Prefer unknown over any — use type guards to narrow instead of bypassing the type system
  • Use branded types for IDs — prevents mixing UserId and OrderId at compile time
  • Return Result<T, E> — makes error handling explicit; callers can't forget to check
  • Cleanup side effects in hooks — every addEventListener or subscribe must 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.

Get the Full Kit →

Or grab the entire Frontend Developer Pro bundle (11 products) for $129 — save 30%.

Get the Complete Bundle →


Related Articles

Top comments (0)