# Replace Type Code with Subclasses

> Turn each value of an immutable type code that drives conditionals into a subclass, and move the branch logic into overrides.

- kind: technique
- category: organizing-data
- aliases: Replace Type Code with Polymorphism
- canonical: https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses
- inspired-by: https://refactoring.guru/replace-type-code-with-subclasses

## Problem

A class has a type field, and several methods `switch` on it. Each new type means editing every switch. Some fields only make sense for one type and sit unused for the others.

## Solution

**Create a subclass for each type value, move each branch into an override in its subclass, and create instances through a factory.** Use this when the type of an object never changes after creation.

## Before

```ts
class Notification {
  constructor(
    readonly channel: "email" | "sms" | "push",
    readonly recipient: string,
    readonly body: string,
    readonly deviceToken?: string, // push only
  ) {}

  maxLength(): number {
    switch (this.channel) {
      case "email": return 100_000;
      case "sms": return 160;
      case "push": return 240;
    }
  }

  send(gateway: Gateway): Promise<void> {
    switch (this.channel) {
      case "email": return gateway.email(this.recipient, this.body);
      case "sms": return gateway.sms(this.recipient, this.body.slice(0, 160));
      case "push": return gateway.push(this.deviceToken!, this.body);
    }
  }
}
```

## After

```ts
abstract class Notification {
  constructor(readonly recipient: string, readonly body: string) {}
  abstract maxLength(): number;
  abstract send(gateway: Gateway): Promise<void>;

  static create(channel: string, recipient: string, body: string, deviceToken?: string): Notification {
    switch (channel) {
      case "email": return new EmailNotification(recipient, body);
      case "sms": return new SmsNotification(recipient, body);
      case "push": return new PushNotification(recipient, body, deviceToken!);
      default: throw new Error(`unknown channel ${channel}`);
    }
  }
}

class EmailNotification extends Notification {
  maxLength() { return 100_000; }
  send(g: Gateway) { return g.email(this.recipient, this.body); }
}

class SmsNotification extends Notification {
  maxLength() { return 160; }
  send(g: Gateway) { return g.sms(this.recipient, this.body.slice(0, this.maxLength())); }
}

class PushNotification extends Notification {
  constructor(recipient: string, body: string, readonly deviceToken: string) { super(recipient, body); }
  maxLength() { return 240; }
  send(g: Gateway) { return g.push(this.deviceToken, this.body); }
}
```

## Why

- Adding a channel is a new subclass plus one factory line; existing classes stay untouched.
- `deviceToken` exists only where it means something, and is no longer optional.
- Each channel's rules can be read and tested on their own.

## Steps

1. Wrap construction in a factory method (`create`) so callers stop calling the constructor with a type code. Run the tests.
2. Self-encapsulate the type field: replace direct reads with a getter.
3. Create one subclass per type value. Override the getter to return its constant. Make the factory return the subclass. Run the tests.
4. For each method that switches on the type, copy each branch into an override in the matching subclass. Make the base method abstract. Run the tests after each method.
5. Move type-specific fields down into their subclass.
6. Remove the type field from the base if nothing reads it any more.

## Watch out

- Requires the type to be fixed for the object's lifetime. If an order can go from `"draft"` to `"paid"`, use [Replace Type Code with State/Strategy](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-state-strategy.md).
- Requires that the class is not already subclassed along another axis. If it is, composition (State/Strategy) avoids a combinatorial explosion.
- One switch over three stable values may be clearer than three classes. In TypeScript, a discriminated union with exhaustive `switch` is often the idiomatic alternative, and the compiler checks completeness.
- The factory still has a switch. That is fine; it is the one place that maps codes to types.
- The inverse is [Replace Subclass with Fields](https://aipatterns.dev/refactoring/techniques/replace-subclass-with-fields.md).

## Related

- Fixes [Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md), [Primitive Obsession](https://aipatterns.dev/refactoring/smells/primitive-obsession.md) and [Temporary Field](https://aipatterns.dev/refactoring/smells/temporary-field.md).
- Inverse: [Replace Subclass with Fields](https://aipatterns.dev/refactoring/techniques/replace-subclass-with-fields.md).
- Uses [Replace Conditional with Polymorphism](https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism.md) for each switch, and a [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md) for creation.

## Agent checklist

- [ ] The type code never changes after construction.
- [ ] The class has no other subclass hierarchy that would multiply.
- [ ] Construction goes through one factory; no caller passes type codes to constructors.
- [ ] Every switch on the type was replaced with overrides, and type-only fields moved down.
- [ ] A discriminated union was considered as a lighter alternative.
