@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.
The scenario: a custom field, typed end-to-end
Say you added a custom field on the backend, such asis_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
TheProductDTO 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
Now the whole SDK is typed
With that one file in place, no cast is needed anywhere:Every product endpoint carries the field
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 alink, 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)
ProductDTO, whether a page, a hook, or a column renderer, sees product.brand resolved, with the data already fetched by the link:
Where each type goes
Checklist
- No
any, and no per-call-site casts for extended data. - Backend additions typed in the panel via a
.d.tsmerging 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.