@mercurjs/types and the typed SDK. When you extend the backend — adding custom fields, a linked module, 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.
The scenario: a custom field, typed end-to-end
Say you added a custom field on the backend — for exampleis_featured on product (see 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
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".
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):
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 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 declares alink, the panel fetches that module’s data alongside the entity (SPEC-021): 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:
src/custom-fields/product.tsx — declare the link (runtime)
src/types/brand.d.ts — declare the shape (types)
ProductDTO — a page, a hook, a column renderer — sees product.brand resolved, with the data already fetched by the link:
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 |
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.