Skip to main content
The panels are fully typed against the API through @mercurjs/types and the typed SDK. When you extend the backend with custom fields, a linked module, or an extra field on a DTO, those additions are not in the shipped types yet. You close the gap in the frontend with a small declaration-merging .d.ts file. Write it once, and every SDK call that returns the entity carries your field, typed.
Never use any to paper over a missing field. Casting a response to any, or to as { custom_fields: … } at each call site, throws away type-checking and has to be repeated everywhere. Augment the type once instead.

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

Say you added a custom field on the backend, such as is_featured on product (see Custom fields). The value now comes back from the API, but the panel’s ProductDTO does not 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". Everything downstream refers back to that same interface: @mercurjs/types, the SDK response wrappers such as AdminProductResponse and list responses, and the panel hooks. 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 is picked up by the app’s tsconfig.
apps/vendor/src/types/custom-fields.d.ts
Follow two rules, or the augmentation silently does nothing:
  • The module name in declare module "..." must be the package that declares the interface you are merging into. Here that is @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.

Now the whole SDK is typed

With that one file in place, no cast is needed anywhere:
Every product endpoint carries the field
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 add the fields to the built-in panel fetches rather than hand-adding +field.*. The vendor product query in particular rejects arbitrary *-relation overrides.

Linked data resolves the same way

The augmentation is not limited to a custom field’s own value. It is how you make linked-module data typed too. When a custom-fields config declares a link, the panel fetches that module’s data alongside the entity (see panel extensions). A link: "brand" merges brand.* into the built-in product fetch for you, with 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.
src/custom-fields/product.tsx: declare the link (runtime)
src/types/brand.d.ts: declare the shape (types)
Now any code that imports ProductDTO, whether a page, a hook, or a column renderer, sees product.brand resolved, with the data already fetched by the link:
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, then consume it as a plain typed property everywhere.

Where each type goes

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.

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.

Next steps

Custom fields

Add fields, rows, actions, and columns to a built-in model, and declare a link.

Module links

Link a custom module to a built-in entity and fetch its data alongside.

Panel extensions

See how the panel fetches linked module data alongside the entity.