← blog

the art of doing less

On writing minimal TypeScript utilities — and why the best abstractions are the ones you almost didn't write.

There's a particular satisfaction in deleting code. Not breaking it, not rewriting it — just removing it entirely because you realised it was solving a problem that didn't need solving.

I've been thinking about this a lot while working on small utility functions. The ones that seem helpful in the abstract but accumulate hidden cost over time.

Consider a function like this:

typescriptfunction formatDate(date: Date, options?: Intl.DateTimeFormatOptions): string {
  return date.toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    ...options,
  });
}

It looks innocent. But the moment you ship it, you've created a contract. Future callers will rely on it. You'll need to handle edge cases — what if date is actually a string? What about timezones? Before long it becomes this:

typescriptfunction formatDate(
  date: Date | string | number,
  options?: Intl.DateTimeFormatOptions,
  locale = 'en-US',
): string {
  const d = date instanceof Date ? date : new Date(date);
  if (isNaN(d.getTime())) throw new RangeError(`Invalid date: ${date}`);
  return d.toLocaleDateString(locale, {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    ...options,
  });
}

Is this better? In isolation, maybe. In a real codebase, it's an abstraction that now needs tests, documentation, and a decision about what to do when it inevitably doesn't handle someone's edge case.

The alternative — just calling date.toLocaleDateString(...) directly at the three places you actually need it — has zero maintenance surface.

The rule I keep coming back to: don't abstract until the duplication is painful and the abstraction is obvious. Three similar-looking lines of code is not sufficient justification. The abstraction should name something that genuinely exists in your domain, not just collapse repeated keystrokes.

Here's what this looks like in practice. Suppose you're building a small API client:

typescriptasync function fetchUser(id: string) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json() as Promise<User>;
}

async function fetchPost(id: string) {
  const res = await fetch(`/api/posts/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json() as Promise<Post>;
}

The duplication is real. But the right abstraction here might not be a fetchResource wrapper — it might be just extracting the error check:

typescriptasync function checked(res: Response): Promise<Response> {
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res;
}

const user = await fetch(`/api/users/${id}`).then(checked).then(r => r.json() as Promise<User>);
const post = await fetch(`/api/posts/${id}`).then(checked).then(r => r.json() as Promise<Post>);

The abstraction is minimal, names a real concept (a checked response), and doesn't obscure the shape of the code.

This is the art: finding the smallest unit of abstraction that earns its place. Not zero — that leads to copy-paste sprawl. Not too much — that leads to the kind of helper libraries that outlive their usefulness by years.

The codebase that's easiest to work in isn't the most cleverly abstracted. It's the one where every function exists for a reason, and you can read the reason off the shape of the code itself.

Delete more. Extract less. Name things that actually exist.