# Introduce Local Extension

> Wrap or subclass a class you cannot change in your own class that adds the methods you need.

- kind: technique
- category: moving-features-between-objects
- canonical: https://aipatterns.dev/refactoring/techniques/introduce-local-extension
- inspired-by: https://refactoring.guru/introduce-local-extension

## Problem

A vendor or framework class lacks several operations you need. You have a growing pile of loose helper functions for it, scattered across modules, each taking the foreign object as a parameter.

## Solution

**Create a class of your own that extends or wraps the foreign class, and put the missing methods there.** Use a subclass when the foreign class allows it and its instances are created by you; use a wrapper when instances come from elsewhere or the class is final.

## Before

```ts
// helpers scattered across the codebase
function isRefundable(charge: Stripe.Charge, now: Date): boolean {
  return !charge.refunded && now.getTime() / 1000 - charge.created < 180 * 86_400;
}
function amountInMajorUnits(charge: Stripe.Charge): number {
  return charge.amount / 100;
}
function customerId(charge: Stripe.Charge): string | null {
  return typeof charge.customer === "string" ? charge.customer : charge.customer?.id ?? null;
}
```

## After

```ts
// charge.ts - local extension of a vendor type we do not control
export class Charge {
  constructor(readonly raw: Stripe.Charge) {}

  get id() { return this.raw.id; }

  isRefundable(now: Date): boolean {
    return !this.raw.refunded && now.getTime() / 1000 - this.raw.created < 180 * 86_400;
  }

  amountInMajorUnits(): number {
    return this.raw.amount / 100;
  }

  customerId(): string | null {
    const c = this.raw.customer;
    return typeof c === "string" ? c : c?.id ?? null;
  }
}

// at the boundary where charges arrive
const charge = new Charge(await stripe.charges.retrieve(id));
```

## Why

- All behavior for the vendor type has one home and one test file.
- Callers discover operations through autocomplete instead of grepping for helpers.
- The wrapper isolates you from vendor shape changes. A field rename in the SDK touches one class.

## Steps

1. Choose subclass or wrapper. Prefer a wrapper for data you receive from SDKs, APIs or ORMs, since you do not control their construction.
2. Create the extension class. For a wrapper, store the original as a field and add accessors for what callers need.
3. Move each existing foreign method into the class with [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md). Run the tests after each.
4. Convert at the boundary: wrap foreign objects where they enter your code.
5. Update callers to use the extension's methods. Delete the loose helpers.

## Watch out

- Wrappers break identity: `new Charge(x) !== new Charge(x)`, and code that expects the vendor type will not accept the wrapper. Keep `raw` available for those calls.
- Don't forward every method of a large vendor class. Expose what you use; the rest stays on `raw`.
- Subclassing a class you don't control couples you to its internals, and some SDKs return plain objects, so a subclass never gets instantiated.
- One or two helpers don't justify a class. Use [Introduce Foreign Method](https://aipatterns.dev/refactoring/techniques/introduce-foreign-method.md).
- No direct inverse; to undo, turn methods back into functions and delete the wrapper.

## Related

- Fixes [Incomplete Library Class](https://aipatterns.dev/refactoring/smells/incomplete-library-class.md) and [Duplicate Code](https://aipatterns.dev/refactoring/smells/duplicate-code.md).
- Grows out of [Introduce Foreign Method](https://aipatterns.dev/refactoring/techniques/introduce-foreign-method.md).
- A wrapper that adds behavior while keeping the interface is a [Decorator](https://aipatterns.dev/design-patterns/decorator.md); one that changes the interface is an [Adapter](https://aipatterns.dev/design-patterns/adapter.md).

## Agent checklist

- [ ] Subclass vs wrapper was chosen based on who constructs the instances.
- [ ] Foreign objects are wrapped once, at the boundary, not throughout the code.
- [ ] The extension exposes only members callers actually use, plus the raw object.
- [ ] Loose helper functions were moved in and deleted.
- [ ] Tests pass.
