Skip to main content
CleanCodeMastery

Replace Type Code with Class: Give Every Magic Number a Proper Badge

Learn the Replace Type Code with Class refactoring with a school house story, before/after TypeScript, C# smart enums, and a clear guide on when to pick Class, Subclasses, or State/Strategy.

26 min read Updated June 11, 2026beginner
refactoringtype codeenumssmart enumprimitive obsessiontypescriptcsharp

๐Ÿซ The mystery of house number 2

Let me tell you about a school in Jaipur. Like many Indian schools, it has four houses: Red, Blue, Green, and Yellow. Every student belongs to one house. House students sit together on Sports Day, earn house points, and wear house badges.

Now here is the problem. Years ago, somebody decided to write houses as numbers on all the school forms. Red was 1, Blue was 2, Green was 3, Yellow was 4. Simple, they thought. Saves ink, they thought. That somebody was Mr. Saxena, the old office clerk. He kept the full mapping safely โ€” in his head. Then he retired, took his head home with him, and the school has been guessing ever since.

One Monday morning, the new clerk, Mr. Verma, is preparing the Sports Day list. A form in front of him says "House: 2". He stops. Is 2 Blue or is 2 Green? He asks Ramu, the peon, who has been in the school for twenty years. Ramu says Green, full confidence. He asks Ms. Kapoor, the PT teacher. She says Blue, equal confidence. There is no chart on any wall. Two senior people, two opposite answers, one poor clerk in the middle.

And worse things have already happened. Last year a form came in with "House: 7". There is no house 7. Nobody invented a fifth house. But the form was accepted, typed into the computer, and printed in the annual report. A student named Ravi was listed for a whole year under a house that does not exist. Another form had "House: 0" because a parent left the box empty and the computer treated empty as zero. Zero-house Ravi and seven-house Ravi both lived peacefully inside the database, because to the database they were just numbers, and numbers never complain.

Mr. Verma finally walks to the neighbouring school to see how they manage. There, every student wears a proper house badge โ€” a small stitched shield that says "BLUE HOUSE" in bold letters with the house colour behind it. You cannot make a fake badge saying "House 7" because the badge maker only produces the four real designs. You never wonder what a badge means, because the meaning is printed on it. You never ask the peon. Wrong values are simply impossible to manufacture.

That bare number 2 on the form is what programmers call a type code. The proper badge is a class. Today's refactoring, Replace Type Code with Class, is exactly the act of replacing the mystery number with the badge.

Figure 1: A quick audit of the old admission form โ€” magic numbers everywhere, meanings nowhere

Before we touch any code, walk through Mr. Verma's term once, because his journey is the journey of every developer who has ever inherited a codebase full of bare numbers.

Figure 2: Mr. Verma's journey from guessing numbers to trusting badges

๐Ÿ” What is Replace Type Code with Class?

A type code is a primitive value โ€” an int, a string, a magic constant โ€” that stands for "what kind of thing this is". Blood group 0, 1, 2, 3. Account tier "GOLD". House 2. The value is just a label, but it is stored as a bare primitive.

Why is that bad? Because a primitive accepts anything. If a field is number, then 7, -3, and 42 are all welcome, even when only 1 to 4 make sense. The compiler cannot help you, because to the compiler your house number and your shoe size are the same thing: just numbers. Validation gets copy-pasted everywhere the value is set, and one day someone forgets one copy โ€” that is exactly how house 7 entered the annual report. And the meaning of 2 lives only in people's memory, which retires along with Mr. Saxena.

Replace Type Code with Class says: create a small, dedicated class for the type code. The class:

  1. Closes the value set. Only the predefined instances โ€” House.RED, House.BLUE, House.GREEN, House.YELLOW โ€” can ever exist. The constructor is private, so nobody can manufacture a fake House(7).
  2. Names every value. No more guessing, no more asking the peon. House.BLUE reads like English.
  3. Centralizes validation. Parsing a raw code from a form or database happens in one method, in one place.
  4. Appears in signatures. A function that takes a House cannot be given a shoe size by mistake. The compiler now stands guard at the gate, like the strictest watchman the school never had.

This refactoring comes from Martin Fowler's classic catalog in Refactoring, and it is the gentlest member of a family of three. Its siblings are Replace Type Code with Subclasses and Replace Type Code with State/Strategy. We will learn how to choose between them in a moment โ€” that choice is the most important lesson on this page, and it is shared across all three posts on purpose.

๐Ÿ’ก

One-line summary: Replace Type Code with Class turns a "magic" primitive label into a small class with named, predefined instances, so that illegal values become impossible to even write.

One key fact before we go on. This refactoring is the right pick only when the type code carries no behaviour. In our school, every house behaves the same way in the software โ€” houses are pure labels. No method anywhere says "if Blue house, compute marks differently". The moment behaviour starts to vary by the code, you have outgrown this refactoring, and you need one of the siblings.

College corner: what we are really doing here is a small dose of domain-driven design. The House class is a value object โ€” an immutable type defined entirely by its value, with no separate identity. The deeper principle is the famous slogan make illegal states unrepresentable: instead of writing runtime checks that reject house 7 after it arrives, we design a type in which house 7 has no possible spelling. Type systems are not just for catching typos; they are a tool for encoding business rules so that violations fail at compile time, the cheapest possible moment to fail.

๐Ÿšจ When do we need it?

Watch for these signs in your code:

  • A field is a primitive but means a category. bloodGroup: number, tier: string, house: number. The type says "any number", but the meaning says "one of four things". This mismatch is the classic Primitive Obsession smell โ€” using simple primitives for ideas that deserve their own type.
  • Magic constants hang around. const RED = 1; const BLUE = 2; near the top of a file, with prayers that everyone uses them and nobody hard-codes a 2 directly. Somebody always hard-codes the 2 directly.
  • The same validation appears in many places. if (house < 1 || house > 4) throw ... copy-pasted in three services, and missing from the fourth โ€” which is, of course, the one the bad form came through.
  • Wrong values have actually leaked in. Your database has a house = 7 row, or a tier = "GLOD" typo, and nobody knows how it got there or how many reports it has already poisoned.
  • Function calls are unreadable. registerStudent("Asha", 2) โ€” what is 2? You must open another file, or ask the team's Mr. Saxena, to find out.

What you should not see, if this refactoring is the right one, is behaviour branching on the code. If you spot switch (house) ladders that make the program do different things per house, that is the Switch Statements smell, and a plain value class will not cure it โ€” the switches will survive the operation. For that you need the polymorphic siblings. Here, our enemy is only the unsafe, unnamed, unvalidated primitive.

A small honest note: if your set of values is tiny, stable, and truly behaviour-free, a built-in language enum may be all you need. Think of the enum as the ready-made badge from the market, and the hand-written class as the tailor-made badge with extra pockets. We will see both, in TypeScript, C#, and Python.

๐Ÿ”„ Before and after at a glance

Here is the school's student record, before the refactoring. Houses are bare numbers, hopes are high, safety is zero.

// BEFORE: a type code โ€” any number can sneak in
const HOUSE_RED = 1;
const HOUSE_BLUE = 2;
const HOUSE_GREEN = 3;
const HOUSE_YELLOW = 4;
 
class Student {
  constructor(
    public name: string,
    public house: number, // hopes and prayers only
  ) {}
}
 
const asha = new Student("Asha", 2); // is 2 Blue? Green? Who knows.
const broken = new Student("Ravi", 7); // compiles happily. House 7!

And after. The house becomes a proper class with a closed set of instances.

// AFTER: a class โ€” only the four real houses can ever exist
class House {
  static readonly RED = new House("RED", "Red House");
  static readonly BLUE = new House("BLUE", "Blue House");
  static readonly GREEN = new House("GREEN", "Green House");
  static readonly YELLOW = new House("YELLOW", "Yellow House");
 
  private constructor(
    readonly code: string,
    readonly displayName: string,
  ) {}
 
  toString(): string {
    return this.displayName;
  }
}
 
class Student {
  constructor(
    public name: string,
    public house: House, // the compiler now stands guard
  ) {}
}
 
const asha = new Student("Asha", House.BLUE); // reads like English
// new Student("Ravi", 7)  -> compile error. House 7 cannot exist.

Notice three things. The constructor is private, so the only House objects in the whole universe are the four static readonly ones โ€” exactly like the badge maker who only owns four designs. The field type changed from number to House, so wrong values fail at compile time, not at Sports Day. And House.BLUE carries its own display name โ€” the badge has the meaning printed on it, so nobody ever asks Ramu the peon again.

Figure 3: Before, any number flows into the house field; after, only four sealed instances exist

The class structure itself is tiny โ€” that is part of its charm. One small class, four sealed instances, one association.

Figure 4: The House value class โ€” a closed set of named instances that Student points to

๐Ÿงญ Which of the three should I pick?

This is the heart of the whole topic, so please read this section slowly. There are three type-code refactorings, and students mix them up all the time. The good news: choosing is easy if you ask just two questions.

Question 1: Does behaviour vary by the code? That is โ€” does any method do different work depending on the value? (Look for switch (type) or if (type === ...) ladders.)

Question 2: Can the type change at runtime? That is โ€” can the same object move from one type to another during its life?

Behaviour varies by code?Type can change at runtime?Pick this refactoringEveryday example
No โ€” pure labelDoesn't matterReplace Type Code with Class (or a plain enum)School house badge โ€” Red, Blue, Green, Yellow; all behave the same
YesNo โ€” fixed for the object's whole lifeReplace Type Code with SubclassesDay-scholar vs boarder โ€” different fees and timings, but a student record never flips type
YesYes โ€” the same object switches typeReplace Type Code with State/StrategyA SIM that moves from prepaid to postpaid โ€” same number, new behaviour
Figure 5: The decision flowchart โ€” two questions pick the right refactoring every time

Our school house is the first row. Houses are labels. No method computes marks differently for Blue house, and even though a student could in theory be moved between houses, nothing about the software's behaviour would change โ€” only the label. So a value class (or enum) is exactly enough โ€” pulling in subclasses here would be like hiring four separate principals to manage four badge colours.

You can also picture the choice as a map with two axes. How different is the behaviour, and how mobile is the type? Each refactoring owns one corner.

Figure 6: The three refactorings on one map โ€” house badges sit in the calm bottom-left corner

Keep this table and map in your pocket. The two sibling posts repeat them, so whichever post you land on, you will always know which tool fits. The top-left "Watch out" corner is rare but real: behaviour is the same everywhere yet the label keeps changing โ€” usually a sign that the label is plain mutable data, not a type code at all.

College corner: in TypeScript you will often reach for a union type before a class: type House = "RED" | "BLUE" | "GREEN" | "YELLOW". Unions close the value set and give superb exhaustiveness checking โ€” a switch over a union with a never-typed default arm fails to compile when a new member appears. The class wins when values must carry data (display names, colours) and behaviour helpers, and when you want one single parsing point. Many mature codebases use both: a union at the JSON boundary, a value class inside the domain.

๐Ÿชœ Step-by-step, the safe way

Never do a refactoring in one giant jump. Move in tiny steps, compiling and running tests after each one. Here is the safe route Mr. Verma's school followed.

Step 1: Create the value class. Private constructor, one static readonly instance per legal value, and a field for the underlying code.

class House {
  static readonly RED = new House(1, "Red House");
  static readonly BLUE = new House(2, "Blue House");
  static readonly GREEN = new House(3, "Green House");
  static readonly YELLOW = new House(4, "Yellow House");
 
  private constructor(
    readonly legacyCode: number, // keep the old number for boundaries
    readonly displayName: string,
  ) {}
}

Step 2: Add conversion helpers. One method parses the old primitive into the class; one property formats it back. All validation now lives in exactly one place.

class House {
  // ...instances as above...
 
  static fromCode(code: number): House {
    const found = [House.RED, House.BLUE, House.GREEN, House.YELLOW]
      .find((h) => h.legacyCode === code);
    if (!found) {
      throw new Error(`Unknown house code: ${code}`);
    }
    return found;
  }
}

Step 3: Add the new field beside the old one. This is the intermediate stage. The Student keeps its old houseCode: number for a short while, and gains a house: House that is derived from it. Old callers keep working; nothing breaks.

class Student {
  house: House; // new
 
  constructor(
    public name: string,
    public houseCode: number, // old โ€” still alive during migration
  ) {
    this.house = House.fromCode(houseCode);
  }
}

Step 4: Move callers over, one by one. Wherever code reads or writes the raw number, change it to use the class. student.houseCode === 2 becomes student.house === House.BLUE. Compile and test after each caller.

Step 5: Delete the old field and the magic constants. When a search shows no one touches houseCode or HOUSE_BLUE = 2 anymore, remove them. The primitive survives only at I/O boundaries โ€” the database column, the API JSON โ€” where House.fromCode and house.legacyCode translate at the edge.

The migration itself is a tiny state machine. Your codebase moves through three clear stages, and you should always know which stage you are standing in.

Figure 7: The migration has three stages โ€” never jump from the first to the last in one move
โš ๏ธ

Do not skip the side-by-side stage in Step 3. If you delete the old number field on day one, every caller breaks at once, and you will be fixing fifty compile errors in a panic. Keeping old and new together for a short time lets you migrate calmly, one caller at a time, with green tests in between. Also be careful with equality: if your class instances may be re-created (for example after JSON deserialization), compare by code, not by object reference.

๐Ÿ A bigger real-life example

Let us make the example more real. The Jaipur school's new software handles admissions. Before the refactoring, the code looked like this โ€” and it had a real bug in production:

// BEFORE: numbers everywhere, validation nowhere
function registerStudent(name: string, house: number, classLevel: number) {
  // someone forgot validation here
  db.save({ name, house, classLevel });
}
 
function sportsDayList(students: { name: string; house: number }[]) {
  // 2... was that Blue or Green? The author guessed. The author guessed WRONG.
  return students.filter((s) => s.house === 2); // meant Green, got Blue
}
 
registerStudent("Ravi", 7, 6); // house 7 saved into the database. Oops.

Two classic disasters: a nonsense value 7 was saved because validation was forgotten in one of many entry points, and a developer guessed the meaning of 2 and guessed wrong. Both bugs are impossible after the refactoring:

// AFTER: the House class guards the gates
class House {
  static readonly RED = new House(1, "Red House", "#d32f2f");
  static readonly BLUE = new House(2, "Blue House", "#1976d2");
  static readonly GREEN = new House(3, "Green House", "#388e3c");
  static readonly YELLOW = new House(4, "Yellow House", "#fbc02d");
 
  static readonly ALL = [House.RED, House.BLUE, House.GREEN, House.YELLOW];
 
  private constructor(
    readonly legacyCode: number,
    readonly displayName: string,
    readonly bannerColor: string, // data per value lives ON the value
  ) {}
 
  static fromCode(code: number): House {
    const found = House.ALL.find((h) => h.legacyCode === code);
    if (!found) throw new Error(`Unknown house code: ${code}`);
    return found;
  }
}
 
function registerStudent(name: string, house: House, classLevel: number) {
  db.save({ name, house: house.legacyCode, classLevel }); // primitive only at the edge
}
 
function sportsDayList(students: { name: string; house: House }[]) {
  return students.filter((s) => s.house === House.GREEN); // no guessing possible
}
 
// registerStudent("Ravi", 7, 6)        -> compile error
registerStudent("Ravi", House.fromCode(formValue), 6); // bad form data fails LOUDLY, in one place

Look at the small extras the class quietly gave us. Each house carries its bannerColor, so the Sports Day page can paint banners without another lookup table floating around. House.ALL gives a ready list for dropdown menus. And the only place the raw number 7 can attack is fromCode, where it is caught immediately with a clear message โ€” not discovered months later in the annual report.

Trace the two paths through the new gate โ€” the good code and the bad code โ€” and see where each one ends up.

Figure 8: The boundary in action โ€” code 2 becomes a real House, code 7 dies loudly at the door

This is the pattern to remember: rich type inside, primitive only at the boundary. Forms, databases, and JSON speak numbers; your program speaks House. The boundary is one thin line, defended in one place, instead of a hundred scattered checkposts each guarded by hope.

๐Ÿ’ป The same refactoring in C# and Python

C# gives us a ladder of options, from lightest to richest.

Option 1: a plain enum. For a small, stable, behaviour-free set, this is often enough:

public enum House
{
    Red = 1,
    Blue = 2,
    Green = 3,
    Yellow = 4
}
 
public class Student
{
    public required string Name { get; init; }
    public required House House { get; init; }
}

This already names the values and shows up in signatures. But know the C# enum's weakness: it is secretly just an integer, and the compiler allows (House)7 without complaint. Enums also cannot carry extra data like a display name or banner colour. So validate at boundaries with Enum.IsDefined, and if you need more, climb the ladder.

Option 2: a hand-rolled value class โ€” the same shape as our TypeScript version, with a private constructor and static readonly instances:

public sealed class House
{
    public static readonly House Red = new(1, "Red House");
    public static readonly House Blue = new(2, "Blue House");
    public static readonly House Green = new(3, "Green House");
    public static readonly House Yellow = new(4, "Yellow House");
 
    public static IReadOnlyList<House> All { get; } = [Red, Blue, Green, Yellow];
 
    public int Code { get; }
    public string DisplayName { get; }
 
    private House(int code, string displayName)
        => (Code, DisplayName) = (code, displayName);
 
    public static House FromCode(int code) =>
        All.FirstOrDefault(h => h.Code == code)
        ?? throw new ArgumentException($"Unknown house code: {code}");
 
    public override string ToString() => DisplayName;
}

Now (House)7 is impossible โ€” there is no cast, no fake instance, no back door. Each value carries data, and parsing lives in one place.

Option 3: a smart enum library. The C# community liked this pattern so much that Steve "Ardalis" Smith packaged it as the Ardalis.SmartEnum NuGet library. You inherit from a base class and get value-equality, FromName, FromValue, lists of all instances, and even JSON and EF Core integration packages for free:

using Ardalis.SmartEnum;
 
public sealed class House : SmartEnum<House>
{
    public static readonly House Red = new(nameof(Red), 1, "Red House");
    public static readonly House Blue = new(nameof(Blue), 2, "Blue House");
    public static readonly House Green = new(nameof(Green), 3, "Green House");
    public static readonly House Yellow = new(nameof(Yellow), 4, "Yellow House");
 
    public string DisplayName { get; }
 
    private House(string name, int value, string displayName)
        : base(name, value) => DisplayName = displayName;
}
 
// House.FromValue(2)        -> House.Blue
// House.FromName("Green")   -> House.Green
// House.List                -> all four houses, ready for a dropdown

The idea is sometimes called an enumeration class, and Microsoft's own microservices guidance recommends it for exactly our reason: when an enum starts wanting validation, data, or behaviour, promote it to a class.

And in Python? Python's enum.Enum is closer to our hand-rolled class than to a C# enum โ€” members are real singleton objects, the set is closed, and you can attach data and helpers:

from enum import Enum
 
class House(Enum):
    RED = (1, "Red House")
    BLUE = (2, "Blue House")
    GREEN = (3, "Green House")
    YELLOW = (4, "Yellow House")
 
    def __init__(self, code: int, display_name: str):
        self.code = code
        self.display_name = display_name
 
    @classmethod
    def from_code(cls, code: int) -> "House":
        for house in cls:
            if house.code == code:
                return house
        raise ValueError(f"Unknown house code: {code}")
 
# House.from_code(2)  -> House.BLUE
# House.from_code(7)  -> ValueError, loudly, in one place

College corner: notice the spectrum across languages. A C# enum is a thin paint coat over an integer โ€” (House)7 slips through, so exhaustive switch expressions still need a discard arm. A Java enum and a Python Enum are true classes with sealed instance sets, which is why Java's switch can be checked for exhaustiveness by the compiler. TypeScript unions get exhaustiveness through the never trick. The lesson for your design brain: "enum" is one word for several very different safety levels, so always ask can a value outside the set be manufactured? If yes, you still need a guarded parsing point. If one day a house does need per-value behaviour โ€” careful! Re-check the decision table first. Light per-value data and helpers fit happily here; genuinely different behaviour per type is a signal to move to Subclasses or State/Strategy.

๐Ÿ“Š Benefits and risks

BenefitsRisks / costs
Illegal values become unrepresentable โ€” House(7) cannot existA new class to write and maintain, plus mapping code at I/O boundaries
Every value has a readable name; code reads like EnglishOverkill for a tiny, stable set โ€” a plain enum may be lighter
Validation lives in one place (fromCode), not scattered at every entry pointSerialization needs care: JSON and databases still speak primitives
The compiler checks signatures โ€” a house cannot be passed where a class level is expectedReference equality can bite if instances are re-created; compare by code
Per-value data (display names, colours) lives on the value itselfDoes not remove behavioural switches โ€” wrong tool if behaviour varies
Leaves a clean seam to grow helpers laterTeam must learn the private-constructor idiom

The payoff is not theoretical. When the Jaipur school's developer looked back over the records, the difference between the number era and the badge era was embarrassing to the numbers.

Figure 9: Wrong house entries reaching the database, before and after the value class

Twelve bad entries a year sounds small until you remember each one is a Ravi standing in the wrong line on Sports Day, a wrong row in the annual report, and an afternoon of office time spent tracing it. After the refactoring the count is zero โ€” not because people became careful, but because carelessness stopped compiling.

๐Ÿงช Which smells does it cure?

SmellHow this refactoring helps
Primitive ObsessionDirectly cures it โ€” the primitive pretending to be a type becomes a real type
Magic numbers and stringsEach mystery value gets a named, sealed instance
Duplicated validationAll parsing and checking funnels into one fromCode method
Switch StatementsOnly reduces the pressure โ€” switches over a closed, named set are safer, but behavioural switches need the sibling refactorings to disappear
Shotgun surgery on value changesAdding or renaming a value touches one class, not twenty files

๐Ÿ—บ๏ธ The whole family in one picture

Before the revision card, here is the entire type-code family on a single mental map. If you remember only one image from these three posts, remember this one.

Figure 10: The type-code family map โ€” match the situation to the branch

๐Ÿ“ฆ Quick revision box

+----------------------------------------------------------------+
|        REPLACE TYPE CODE WITH CLASS - REVISION CARD            |
+----------------------------------------------------------------+
| Problem  : a primitive (int/string) secretly means a category  |
|            -> any value sneaks in, meaning lives in memory     |
| Solution : small class, PRIVATE constructor,                   |
|            static readonly instance per legal value            |
| Result   : illegal values cannot even be written               |
|                                                                |
| WHICH OF THE THREE?                                            |
|   no behaviour varies            -> CLASS / ENUM   (this one)  |
|   behaviour varies, type fixed   -> SUBCLASSES                 |
|   behaviour varies + type changes-> STATE / STRATEGY           |
|                                                                |
| Remember : rich type inside, primitive only at the boundary    |
| C# bonus : plain enum -> hand-rolled class -> Ardalis.SmartEnum|
+----------------------------------------------------------------+

โœ๏ธ Practice exercise

Your turn. A hospital app stores blood groups as numbers: 0 = O, 1 = A, 2 = B, 3 = AB. The code below has already accepted a patient with blood group 9.

const O = 0, A = 1, B = 2, AB = 3;
 
class Patient {
  constructor(
    public name: string,
    public bloodGroup: number, // danger!
  ) {}
}
 
const p = new Patient("Meena", 9); // compiles. 9 is not a blood group.

Do the refactoring yourself, step by step:

  1. Create a BloodGroup class with a private constructor and four static readonly instances: O, A, B, AB. Give each a code: number and a label: string (like "AB").
  2. Add BloodGroup.fromCode(code: number) that throws a clear error for anything outside 0โ€“3, and a BloodGroup.ALL list.
  3. Change Patient.bloodGroup to type BloodGroup, keeping the old numeric field briefly while you migrate callers โ€” remember the three migration stages from Figure 7.
  4. Prove the win: show that new Patient("Meena", 9) no longer compiles, and that BloodGroup.fromCode(9) fails loudly with a clear message.
  5. Bonus thinking: the hospital now wants a rule โ€” "group O can donate to everyone". Is that data on the value (fine here) or behaviour that varies by type (a hint to look at the decision table again)? Write one sentence defending your answer.

If you can explain to a friend why step 4's compile error is a gift, you have understood this refactoring completely. And when your type code starts genuinely behaving differently per value, walk straight into the next post: Replace Type Code with Subclasses, where three kinds of students pay three kinds of fees.

Frequently asked questions

What exactly is a type code?
A type code is a plain number or string that secretly stands for a kind of thing โ€” like 2 meaning Blue house or the string GOLD meaning a gold account. The computer sees only a primitive value, so it cannot stop wrong values or explain what the value means.
When should I use Replace Type Code with Class instead of Subclasses or State/Strategy?
Use it when the code is only a label and no behaviour changes based on it. If behaviour varies but the type is fixed for the object's whole life, use Subclasses. If behaviour varies and the type can change at runtime, use State/Strategy.
Is a plain enum enough instead of a full class?
Often yes. For a small, stable set of values with no extra data or rules, a language enum is lighter and perfectly fine. Reach for a hand-made class or a smart enum when you need validation, extra fields like display names, or helper methods on each value.
Does this refactoring remove switch statements?
Not by itself. It closes the value set and names the values, which makes switches safer. But if methods still branch on the type to behave differently, you need Replace Type Code with Subclasses or State/Strategy to remove those switches.
What happens at the database or API boundary where only numbers exist?
Keep small conversion helpers at the edge. A fromCode method parses the raw number or string into the class instance, and a code property formats it back. Inside the program everyone uses the rich type; only the boundary touches the primitive.

Further reading

Related Lessons