> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mercurjs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Types & augmentation

> Type the panels against your own backend extensions — augment framework DTOs with declaration merging so every SDK endpoint carries your custom data.

The panels are fully typed against the API through `@mercurjs/types` and the typed SDK. When you extend the backend — adding [custom fields](/rc/resources/best-practices/custom-fields), a [linked module](/rc/resources/best-practices/module-links), an extra field on a DTO — those additions aren't in the shipped types yet. You close the gap in the **frontend** with a small declaration-merging `.d.ts` file. Do that once and *every* SDK call that returns the entity carries your field, typed.

<Warning>
  **Never use `any` to paper over a missing field.** Casting a response to `any` (or `as { custom_fields: … }` at each call site) throws away type-checking and has to be repeated everywhere. Augment the type once instead.
</Warning>

## The scenario: a custom field, typed end-to-end

Say you added a custom field on the backend — for example `is_featured` on `product` (see [Custom fields](/rc/resources/best-practices/custom-fields)). The value now comes back from the API, but the panel's `ProductDTO` doesn't know about it, so `product.is_featured` is a type error.

Fix it in the panel with a declaration-merging file.

### Why merging works here

The `ProductDTO` the SDK returns ultimately resolves to Medusa's upstream `ProductDTO`, which is declared as an **`interface`** in `@medusajs/types`. Interfaces are open, so you can **merge into it** with `declare module "@medusajs/types"`.

Because everything downstream — `@mercurjs/types`, the SDK response wrappers (`AdminProductResponse`, list responses), and the panel hooks — refers back to that same interface, your added members appear in all of them at once. You augment in one place and every product-returning endpoint is typed.

### Add the `.d.ts` in the panel

Drop a declaration file anywhere under the panel's `src/` (it's picked up by the app's `tsconfig`):

```ts title="apps/vendor/src/types/custom-fields.d.ts" theme={null}
import "@medusajs/types"

declare module "@medusajs/types" {
  interface ProductDTO {
    custom_fields?: {
      is_featured?: boolean
    }
  }
}
```

<Warning>
  Two rules or the augmentation silently does nothing:

  * The module name in `declare module "..."` must be the package that declares the interface you're merging into — here `@medusajs/types`, the owner of `UpstreamProductDTO`, **not** `@mercurjs/types` (which only aliases it).
  * The file must be a module. Add an `import "@medusajs/types"` (or a trailing `export {}`) so TypeScript treats it as one.
</Warning>

### Now the whole SDK is typed

With that one file in place, no cast is needed anywhere:

```ts title="Every product endpoint carries the field" theme={null}
const { products } = await sdk.vendor.products.query()
products[0].custom_fields?.is_featured // ✅ typed, no cast

const { product } = await sdk.vendor.products.$id.query({ $id: id })
product.custom_fields?.is_featured     // ✅ typed everywhere ProductDTO flows
```

<Tip>
  Types and runtime are separate concerns: this `.d.ts` makes the field *typed*, but it only *arrives* if the fetch asks for it. Let the [custom-fields `link` / registry merge](/rc/resources/best-practices/custom-fields#the-extension-api-link-property) add the fields to the built-in panel fetches rather than hand-adding `+field.*` — the vendor product query in particular rejects arbitrary `*`-relation overrides.
</Tip>

## Linked data resolves the same way

The augmentation isn't limited to a custom field's own value — it's how you make **linked-module data** typed too. When a [custom-fields config](/rc/resources/best-practices/custom-fields#the-extension-api-link-property) declares a `link`, the panel fetches that module's data alongside the entity ([SPEC-021](/rc/references/panel-extension-api)): `link: "brand"` merges `brand.*` into the built-in product fetch for you — no hand-written field list.

Pair that one config line with a matching augmentation, and the linked data is both **present at runtime** and **typed everywhere `ProductDTO` is imported**:

```ts title="src/custom-fields/product.tsx — declare the link (runtime)" theme={null}
export default defineCustomFieldsConfig({
  model: "product",
  link: "brand", // brand.* is fetched with every product
  list: {
    columns: [
      { id: "brand_name", header: "Brand", component: ({ row }) => row.brand?.name },
    ],
  },
})
```

```ts title="src/types/brand.d.ts — declare the shape (types)" theme={null}
import "@medusajs/types"

declare module "@medusajs/types" {
  interface ProductDTO {
    brand?: { id: string; name: string }
  }
}
```

Now any code that imports `ProductDTO` — a page, a hook, a column renderer — sees `product.brand` resolved, with the data already fetched by the `link`:

```ts theme={null}
const { product } = await sdk.vendor.products.$id.query({ $id: id })
product.brand?.name // ✅ present (via link) and typed (via augmentation)
```

<Tip>
  The `link` does the fetching, the `.d.ts` does the typing — you write each once, per model, and every product-returning endpoint in the panel is covered. This is the payoff of augmentation: register the relationship in one place, consume it as a plain typed property everywhere.
</Tip>

## Where each type goes

| You're adding…                                      | Put it in                                                                           |
| --------------------------------------------------- | ----------------------------------------------------------------------------------- |
| A field your API now returns on an entity           | A panel `.d.ts` merging into the framework DTO (`declare module "@medusajs/types"`) |
| A one-off request/response shape for a custom route | Infer it from the Zod validator (`z.infer<typeof Schema>`) and export it            |
| A brand-new Mercur/domain DTO                       | The domain folder in `@mercurjs/types`, re-exported from `index.ts`                 |

<Warning>
  DTOs and enums the platform already ships (`ProductDTO`, `SellerStatus`, `MercurModules`, `HttpTypes`) are imported from `@mercurjs/types` — never redeclared. In the dashboards, `HttpTypes` comes from `@mercurjs/types` too, which is what keeps request/response types aligned with Mercur's extended routes.
</Warning>

## Checklist

* No `any`, and no per-call-site casts for extended data.
* Backend additions typed in the panel via a `.d.ts` merging into the **framework** interface that owns the DTO.
* Augmentation files name the correct package and are real modules (`import`/`export {}`).
* Extended fields are requested with `+field.*` so they actually arrive.
* Shipped types imported from `@mercurjs/types`; one-off shapes inferred from Zod.
