Jihye's Blog

image for refactor-types

How I refactored code

How I Refactored Code to Make It Cleaner, Safer, and More Scalable

Photo by Mark König on Unsplash

Recently, I came across some TypeScript code that worked fine, but it felt verbose, hard to maintain, and not as type-safe as it could be. I decided to refactor it, and the results were a much cleaner, safer, and more scalable solution.

Here’s the journey from the original code to the refactored version, and why the new approach is an improvement.

The Original Code

Code
export enum Category {
  Review = "review",
  NextJs = "nextJs",
  React = "react",
  TailwindCss = "tailwindCss",
  TypeScript = "typeScript",
  Frontend = "frontend",
  Backend = "backend",
}

export const categories = Object.values(Category);

export default function getCategory(category: string) {
  switch (category) {
    case "review":
      return Category.Review;
    case "nextJs":
      return Category.NextJs;
    case "react":
      return Category.React;
    case "tailwindCss":
      return Category.TailwindCss;
    case "typeScript":
      return Category.TypeScript;
    case "frontend":
      return Category.Frontend;
    case "backend":
      return Category.Backend;
    default:
      return undefined;
  }
}

export function getRealCategoryName(name: Category | undefined) {
  switch (name) {
    case Category.Review:
      return "Review";
    case Category.NextJs:
      return "Next.js";
    case Category.React:
      return "React";
    case Category.TailwindCss:
      return "Tailwind CSS";
    case Category.TypeScript:
      return "TypeScript";
    case Category.Frontend:
      return "Frontend";
    case Category.Backend:
      return "Backend";
    default:
      return undefined;
  }
}

Problems with this approach

  • Too much boilerplate: Two large switch statements just for mapping values.
  • Error-prone: Adding a new category means updating multiple places.
  • Loose typing: getCategory accepts string, which means invalid values (like "banana") are only caught at runtime.

The Refactored Code

Here’s the updated version:

Code
const Category = {
  Review: "review",
  "Next.js": "nextJs",
  React: "react",
  "Tailwind CSS": "tailwindCss",
  TypeScript: "typeScript",
  Frontend: "frontend",
  Backend: "backend",
} as const;

export type CategoryLabel = keyof typeof Category;
export type CategoryValue = (typeof Category)[CategoryLabel];

const valueToLabel = Object.fromEntries(
  Object.entries(Category).map(([label, value]) => [value, label])
) as Record<CategoryValue, CategoryLabel>;

export const categoryValues = Object.values(Category) as CategoryValue[];

export function getCategoryLabel(value: CategoryValue): CategoryLabel {
  return valueToLabel[value];
}

Why This Is Better

1. Single Source of Truth

All categories are defined in one object (Category). Labels and values stay in sync automatically.

2. Type Safety

With:

Code
export type CategoryValue = (typeof Category)[CategoryLabel];

TypeScript ensures only valid values can be used. Calling getCategoryLabel("banana") is a type error, not a runtime bug.

3. No More Switch Statements

We replaced the giant switch blocks with a reverse mapping built dynamically:

Code
const valueToLabel = Object.fromEntries(...)

This automatically supports new categories without additional code.

4. Object Mapper Technique

What we did here is essentially applying an object mapper technique. Instead of imperatively coding mappings with switch or if/else, we define a single object (Category) and then programmatically derive other useful mappings (valueToLabel, categoryValues). This turns a lot of boilerplate into data-driven code. It’s a declarative style that scales much better.

5. Less Boilerplate, More Scalability

Adding a new category is now as simple as editing the Category object. Everything else updates itself.

Example Usage

Code
getCategoryLabel("nextJs"); // "Next.js"
getCategoryLabel("react"); // "React"

If you try something invalid:

Code
getCategoryLabel("banana"); // ❌ TypeScript error

Final Thoughts

Enums are useful, but in this case, they added unnecessary verbosity. By switching to an as const object with type inference, we ended up with code that is:

  • Cleaner
  • Safer
  • Easier to extend
  • Easier to maintain
  • Powered by an object mapper technique instead of brittle switch statements

Next time you find yourself writing long switch statements just to map enums, consider whether a const object with inferred types and an object mapper might serve you better.