The short answer
Define the types with @typedef in the module they belong to. Re-export them from a types.js module with @typedef {import(…)}, and bring them into any file with @import:
import { orderTotal } from "./orders.js";
/** @import { Order, LineItem } from "./types.js" */
/** @type {LineItem} */
const lamp = { sku: "LAMP-01", cents: 3900 };
/** @type {Order} */
const order = { id: "A-1001", lines: [lamp] };
console.log(orderTotal(order));import { orderTotal } from "./orders.js";
import type { Order, LineItem } from "./types.js";
const lamp: LineItem = { sku: "LAMP-01", cents: 3900 };
const order: Order = { id: "A-1001", lines: [lamp] };
console.log(orderTotal(order));jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} checkout.js imports a real function from orders.js and only types from types.js; the types are checked exactly as if they were declared locally.
Why it works
Every top-level @typedef in a module is exported, so orders.js exports Order and LineItem without any extra syntax. @typedef {import("./orders.js").Order} Order in types.js declares a new type with the same name and shape, which types.js exports in turn: that is a re-export. export {} keeps types.js a module even though it has no values.
@import { Order, LineItem } from "./types.js" is TypeScript 5.5’s JSDoc form of import type. It reads like an import statement, works with several names at once, and imports nothing at runtime.
The same thing in TypeScript
The TypeScript equivalent tab writes import type { Order, LineItem } from "./types.js" and annotates the variables inline. A TypeScript types.ts would re-export with export type { Order, LineItem } from "./orders.js"; in JavaScript the @typedef line does that job.
What goes wrong
A @typedef is not a value, so importing it with a real import statement is an error, and tsc’s message (TS18042) tells you the JSDoc form to use instead. A name the module does not export fails in the @import comment (TS2305):
import { Order } from "./orders.js";error TS18042: 'Order' is a type and cannot be imported in JavaScript files. Use 'import("./orders.js").Order' in a JSDoc type annotation.
/** @import { Invoice } from "./orders.js" */error TS2305: Module '"./orders.js"' has no exported member 'Invoice'.
/** @type {Order} */
const order = { id: "A-1001", total: 0 };
/** @type {Invoice} */
const invoice = { number: 1 };
console.log(order, invoice);jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Gotchas
- Paths are module paths.
@importandimport()types use the same specifiers as real imports: undernodenext, relative paths need their.jsextension. - A script file shares its typedefs globally. In a file without
importorexport, top-level@typedefs are global to the project, which can clash between files;export {}makes the file a module. - Unused
@importnames are not checked. A misspelled name that no type uses is not reported in TypeScript 5.9; it fails as soon as a JSDoc type refers to it.
Questions
Which should I use, @import or @typedef {import(…)}?
@import on TypeScript 5.5 and later: one line brings in several types, like an import type statement. @typedef {import("./orders.js").Order} Order also works on older versions (import types arrived in TypeScript 2.9) and is still the way to re-export a type under a name.
Does @import load the module at runtime?
No. It is a comment; only the checker and the editor read it. The module is loaded only if the file also imports it with a real import statement.
Sources
- TypeScript 5.5 release notes (the @import tag) · TypeScript handbook
- JSDoc Reference (import types) · TypeScript handbook
Last reviewed 21 October 2026.