# Replace Type Code with State/Strategy

> Move behavior that depends on a type code into separate state or strategy objects that the host holds and can swap at runtime.

- kind: technique
- category: organizing-data
- canonical: https://aipatterns.dev/refactoring/techniques/replace-type-code-with-state-strategy
- inspired-by: https://refactoring.guru/replace-type-code-with-state-strategy

## Problem

A class branches on a type code in several methods, and the code changes during the object's life, or the class already has subclasses. [Replace Type Code with Subclasses](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses.md) can't work, because an object cannot change its class.

## Solution

**Create an interface for the type-dependent behavior, one implementation per code, and give the host a field holding the current implementation.** Changing the type code becomes replacing that object.

## Before

```ts
class PurchaseOrder {
  status: "draft" | "submitted" | "approved" = "draft";
  lines: PoLine[] = [];

  addLine(line: PoLine): void {
    if (this.status !== "draft") throw new Error("only drafts can be edited");
    this.lines.push(line);
  }

  submit(): void {
    if (this.status !== "draft") throw new Error(`cannot submit from ${this.status}`);
    if (this.lines.length === 0) throw new Error("empty order");
    this.status = "submitted";
  }

  approve(): void {
    if (this.status !== "submitted") throw new Error(`cannot approve from ${this.status}`);
    this.status = "approved";
  }
}
```

## After

```ts
interface PoState {
  readonly name: string;
  addLine(po: PurchaseOrder, line: PoLine): void;
  submit(po: PurchaseOrder): void;
  approve(po: PurchaseOrder): void;
}

const reject = (from: string, action: string) => () => {
  throw new Error(`cannot ${action} from ${from}`);
};

const Draft: PoState = {
  name: "draft",
  addLine: (po, line) => po.lines.push(line),
  submit: (po) => {
    if (po.lines.length === 0) throw new Error("empty order");
    po.state = Submitted;
  },
  approve: reject("draft", "approve"),
};

const Submitted: PoState = {
  name: "submitted",
  addLine: () => { throw new Error("only drafts can be edited"); },
  submit: reject("submitted", "submit"),
  approve: (po) => { po.state = Approved; },
};

const Approved: PoState = {
  name: "approved",
  addLine: () => { throw new Error("only drafts can be edited"); },
  submit: reject("approved", "submit"),
  approve: reject("approved", "approve"),
};

class PurchaseOrder {
  state: PoState = Draft;
  lines: PoLine[] = [];

  get status() { return this.state.name; }
  addLine(line: PoLine) { this.state.addLine(this, line); }
  submit() { this.state.submit(this); }
  approve() { this.state.approve(this); }
}
```

## Why

- Each status lists what it allows in one place. Adding `"rejected"` is a new object, not edits to every method.
- The host keeps its identity while its behavior changes.
- Works even if `PurchaseOrder` already has subclasses.

## Steps

1. Self-encapsulate the type code so reads and writes go through accessors.
2. Define the state or strategy interface with one method per type-dependent operation.
3. Create one implementation per code. Each starts by returning or throwing what the matching branch did.
4. Add a field on the host for the current object. Make the type-code setter assign the matching implementation, and the getter read its `name`. Run the tests.
5. For each method, move the branch bodies into the implementations and make the host delegate. Run the tests after each method.
6. Remove the raw type-code field.

## Watch out

- State or Strategy? If implementations trigger transitions to each other, as here, it is [State](https://aipatterns.dev/design-patterns/state.md). If an outside caller picks one and it rarely changes, it is [Strategy](https://aipatterns.dev/design-patterns/strategy.md). The mechanics are the same.
- Persist the state's `name`, not the object. Rebuild the state object when loading.
- For two or three states with simple rules, a transition table (`Record<Status, Status[]>`) or a discriminated union may be clearer than objects.
- If the type never changes and the class has no other subclasses, [Replace Type Code with Subclasses](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses.md) is simpler.
- Inverse: collapse states back into a code and conditionals when the variants shrink to trivial differences.

## Related

- Fixes [Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md), [Primitive Obsession](https://aipatterns.dev/refactoring/smells/primitive-obsession.md) and [Large Class](https://aipatterns.dev/refactoring/smells/large-class.md).
- Patterns: [State](https://aipatterns.dev/design-patterns/state.md) and [Strategy](https://aipatterns.dev/design-patterns/strategy.md).
- Siblings: [Replace Type Code with Class](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-class.md) and [Replace Type Code with Subclasses](https://aipatterns.dev/refactoring/techniques/replace-type-code-with-subclasses.md).

## Agent checklist

- [ ] The type changes at runtime, or the host already has subclasses.
- [ ] The interface covers every operation that branched on the type.
- [ ] Transitions happen in exactly one place per state.
- [ ] Persistence stores the code and rebuilds the state on load.
- [ ] Tests cover every allowed and forbidden transition.
