skip to content

In a JavaScript file that TypeScript is checking, how do you annotate a function's parameters and return type, declare a reusable named object type, and make a function generic — using only JSDoc?

level: middleimportance: must knowfreq 54%

answer

  1. types live inside the braces
  2. braces hold real TypeScript type syntax
  3. one tag names a reusable type
  4. one tag introduces a type parameter
  5. casts need parentheses

basics

~20 s

Use JSDoc tags in comments: @param and @returns for a function's signature, @type for a variable or a cast, @typedef to declare a reusable named type, and @template to introduce a type parameter. TypeScript reads them as real types.

solid answer

~50 s

TypeScript understands a subset of JSDoc as genuine type syntax, so a `.js` file can be typed without a single character of TypeScript syntax. You annotate a function with `@param {string} name` and `@returns {number}`; you annotate a declaration with `/** @type {Config} */` above it; you declare a reusable named type with `@typedef` — either an inline object type or a `@typedef {Object} Name` plus `@property` lines; and you make a function generic with `@template T`, then use `T` in the `@param` and `@returns` tags. Types defined elsewhere are pulled in with an import type: `@type {import('./types.js').Config}`, which works for `.ts` and `.d.ts` sources too. The type expressions inside the braces are TypeScript type syntax, so unions, generics and utility types all work. There is also a parenthesised cast form, `/** @type {HTMLInputElement} */ (el)`, and a `@satisfies` tag mirroring the `satisfies` operator.

code

javascript · 26 lines
javascript
// @ts-check

/**
 * @typedef {Object} Job
 * @property {string} id
 * @property {number} retries
 * @property {string[]} [tags]
 */

/**
 * @template T
 * @param {T[]} items
 * @param {(item: T) => boolean} predicate
 * @returns {T | undefined}
 */
function findFirst(items, predicate) {
  for (const item of items) {
    if (predicate(item)) return item;
  }
  return undefined;
}

/** @type {Job[]} */
const jobs = [{ id: "a", retries: 0 }];

export const stuck = findFirst(jobs, (job) => job.retries > 3);

go deeper

for a junior

Recall the everyday tags and what each covers: @param and @returns for a function signature, @type for a variable, and that the text inside the braces is a real type.

for a middle

Explain the fuller toolkit — @typedef for named types, @template for generics, import() types for cross-file reuse, and the parenthesised @type cast — and note that these only bite when the file is checked.

for a senior

Demonstrate judgment about where JSDoc typing pays: a package shipping types without a build step, or code that cannot take .ts yet, weighed against the annotation drift and review cost it carries.

for a principal

Own the standard for a mixed codebase — where the single source of truth for shared types lives, whether JavaScript files may declare types or only consume them, and what would make the JSDoc layer worth retiring.

## Why JSDoc types exist A `.js` file cannot carry type syntax — `function f(x: string)` is a JavaScript syntax error. So TypeScript reads types out of comments instead. The braces in a JSDoc tag do not contain "JSDoc types" in some parallel dialect: they contain **TypeScript type expressions**, checked with the same rules. Unions, generics, `keyof`, template literal types, `Partial<T>` — all of it is available, just written inside a comment. None of this changes the file at runtime; comments are comments. What changes is what the checker knows. ## Annotating a function ```js /** * @param {string} label * @param {number} [count] optional parameter * @param {"asc" | "desc"} [order="asc"] with a default * @returns {string} */ export function format(label, count, order) { return `${label} ${count ?? 0} ${order}`; } ``` `@param` names must match the real parameter names. Square brackets mark a parameter optional. The type expression is ordinary TypeScript, so the union above is a genuine union and passing `"up"` is an error. ## Annotating a declaration, and casting `@type` attaches a type to the declaration below it: ```js /** @type {Map<string, number>} */ const counts = new Map(); ``` Without the tag the inferred type would be `Map<any, any>`. `@type` is also the cast form, applied to a *parenthesised* expression — the parentheses are required: ```js const input = /** @type {HTMLInputElement} */ (document.getElementById("name")); ``` This is the JSDoc spelling of `as`. Like `as`, it is an assertion the checker trusts, not a runtime conversion: nothing verifies the element really is an input, and if it is not, the belief is simply wrong at runtime. `/** @type {any} */ (x)` is the escape hatch of last resort. ## Declaring reusable named types `@typedef` gives a type a name that other tags — and other files — can reference. Two spellings: ```js /** * @typedef {{ id: string, retries: number, tags?: string[] }} Job */ ``` ```js /** * @typedef {Object} Job * @property {string} id * @property {number} retries * @property {string[]} [tags] optional */ ``` The first is compact; the second gives each member its own documentation line. Either way `Job` is now a type name usable in `@param {Job}`, `@type {Job}` and so on. `@callback` does the same job for function types. A `@typedef` in one file is visible to another via an import type, and — this is the important interop point — the same syntax reaches into TypeScript sources: ```js /** @type {import('./types.js').Config} */ const config = loadConfig(); ``` So a JavaScript file can consume types declared in a `.ts` or `.d.ts` file, which is what lets a mixed codebase share one set of type definitions. ## Generics with @template ```js /** * @template T * @param {T[]} items * @param {(item: T) => boolean} predicate * @returns {T | undefined} */ export function findFirst(items, predicate) { for (const item of items) { if (predicate(item)) return item; } return undefined; } ``` `@template` declares the type parameter; every other tag in the same comment can then use it. Constraints are written on the tag, `@template {object} T`, and multiple parameters get multiple tags or one comma-separated list. ## @satisfies JSDoc also has a `@satisfies` tag, mirroring the `satisfies` operator: it checks the value against a type without widening the value's own inferred type, so literal members stay literal while still being validated. It is the right tag for a config object you want both validated and precisely inferred. ```js /** @satisfies {Record<string, string>} */ const routes = { home: "/", about: "/about" }; ``` ## The realistic tradeoffs JSDoc typing is genuinely capable — most of the type system is reachable, including conditional and mapped types written inside a `@typedef`. What you pay for it is verbosity and a weaker feedback loop: the annotation sits away from the parameter it describes, a renamed parameter can leave its `@param` orphaned, and the comment is easy to skip in review. It is worth it in exactly the situation it was designed for — a codebase that cannot take `.ts` files yet, or a published package that wants types without a build step — and less worth it once the file could simply be renamed. One last practical note: none of these tags do anything unless the file is actually being checked. Add `// @ts-check` at the top or turn on `checkJs`, otherwise you have written documentation, not types.

  • How does a JavaScript file reference a type declared in a .ts or .d.ts file?
    With an import type inside the JSDoc braces: `/** @type {import('./types.js').Config} */`. The same form works for a `@typedef` alias, `@typedef {import('./types.js').Config} Config`, after which the short name is usable in every other tag in the file. That is what lets JavaScript and TypeScript files in one repo share a single set of type definitions.
  • Is `/** @type {Foo} */ (expr)` checked at runtime?
    No. It is the JSDoc spelling of `as` — an assertion the checker accepts on your word, erased entirely from the output. Nothing verifies the value's real shape, so an incorrect assertion produces a type the checker believes and the runtime contradicts. If the value's identity is genuinely uncertain, validate it in code rather than asserting.
  • What happens to JSDoc types if the file is not being checked?
    They are inert. Without `checkJs` or a file-level `// @ts-check`, the comments are just documentation — nothing validates that a `@param` type matches the call sites, and a typo in a type name goes unreported. The annotations still feed inference for TypeScript consumers importing the file, but the file itself is not held to them.

saying these in an interview costs you the question

  • Thinks JSDoc braces use a dialect separate from TypeScript types
  • Believes a JSDoc @type cast is verified at runtime
  • Says generics cannot be expressed in JSDoc
  • Forgets the parentheses required around a cast expression
  • Expects JSDoc types to be enforced without checkJs or @ts-check

context