Types in JSDoc
JSDoc type annotations let you get TypeScript's type checking and IDE autocompletion in plain JavaScript files without converting them to .ts. This is useful when you cannot change the file extension, when working in a JS-only project, or when migrating incrementally.
Enabling Type Checking in JS Files
There are two ways to enable TypeScript's type checker in a .js file:
Add // @ts-check at the top of a specific file
Enable checkJs: true in tsconfig.json to check all JS files in the project
// @ts-check // TypeScript now type-checks this file. // Hover over variables in VS Code to see inferred types. const message = 'Hello, world!' // message: string const count = 42 // count: number // This will show a type error: // const result = message + count // OK (string concatenation) // const bad = message.toFixed(2) // Error: toFixed not on string
// @ts-check must be the very first comment in the file (or at least before any code). Without it, TypeScript ignores the file even if checkJs is enabled globally.@type Annotation
The @type tag annotates variables, parameters, and return values. It accepts any TypeScript type expression inside {}.
// @ts-check
/** @type {string} */
let username
/** @type {number | null} */
let age = null
/** @type {string[]} */
const tags = []
/** @type {{ id: number; name: string; active: boolean }} */
const user = { id: 1, name: 'Alice', active: true }
/** @type {Map<string, number>} */
const scores = new Map()
/** @type {Promise<string>} */
const fetchName = fetch('/name').then(r => r.text())
// Import types from TypeScript declaration files
/** @type {import('express').Request} */
let req@param and @returns
Use @param to type function parameters and @returns (or @return) for the return type.
// @ts-check
/**
* Formats a person's full name.
*
* @param {string} firstName
* @param {string} lastName
* @param {{ title?: string }} [options]
* @returns {string}
*/
function formatName(firstName, lastName, options = {}) {
const prefix = options.title ? `${options.title} ` : ''
return `${prefix}${firstName} ${lastName}`
}
const name1 = formatName('Alice', 'Smith')
// name1: string
const name2 = formatName('Bob', 'Jones', { title: 'Dr.' })
// name2: string
/**
* Fetches a user from the API.
*
* @param {string} id
* @returns {Promise<{ id: string; name: string; email: string }>}
*/
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`)
return response.json()
}
// TypeScript knows the resolved type — fully autocompleted
fetchUser('123').then(user => {
console.log(user.name) // string
console.log(user.email) // string
})@typedef for Complex Types
@typedef creates a named type that can be reused across a file (or exported and used in other files).
// @ts-check
/**
* @typedef {Object} User
* @property {string} id
* @property {string} name
* @property {string} email
* @property {number} age
* @property {boolean} [active] - Optional field
* @property {string[]} [roles] - Optional array
*/
/** @typedef {'admin' | 'editor' | 'viewer'} UserRole */
/**
* @typedef {Object} PaginatedResponse
* @property {User[]} data
* @property {number} total
* @property {number} page
* @property {number} pageSize
*/
/**
* @param {User} user
* @returns {string}
*/
function getUserLabel(user) {
return `${user.name} <${user.email}>`
}
/**
* @param {PaginatedResponse} response
* @returns {number}
*/
function getTotalPages(response) {
return Math.ceil(response.total / response.pageSize)
}@template for Generics
@template defines generic type parameters in JSDoc functions, matching TypeScript's <T> syntax.
// @ts-check
/**
* Returns the first element of an array or undefined.
*
* @template T
* @param {T[]} array
* @returns {T | undefined}
*/
function first(array) {
return array[0]
}
const n = first([1, 2, 3]) // n: number | undefined
const s = first(['a', 'b']) // s: string | undefined
const u = first([]) // u: undefined
/**
* Creates a pair of values.
*
* @template T, U
* @param {T} first
* @param {U} second
* @returns {[T, U]}
*/
function pair(first, second) {
return [first, second]
}
const p = pair('hello', 42) // p: [string, number]
/**
* Wraps a value in a Promise.
*
* @template T
* @param {T} value
* @returns {Promise<T>}
*/
function wrap(value) {
return Promise.resolve(value)
}
const wrapped = wrap({ id: 1 }) // Promise<{ id: number }>Importing Types from .d.ts Files
You can import TypeScript types into JSDoc annotations using the import() type syntax. This lets you consume types from TypeScript libraries without converting your file.
// @ts-check
/**
* @param {import('express').Request} req
* @param {import('express').Response} res
* @returns {void}
*/
function handler(req, res) {
const userId = req.params.id // string — typed!
res.json({ userId })
}
// Import from your own .d.ts files
/**
* @param {import('../types').User} user
* @returns {import('../types').UserDto}
*/
function toDto(user) {
return { id: user.id, displayName: user.name }
}
// Reuse an import with @typedef
/** @typedef {import('express').Application} ExpressApp */
/**
* @param {ExpressApp} app
*/
function registerRoutes(app) {
app.get('/', handler)
}@ts-ignore and @ts-expect-error
Sometimes you need to suppress a specific type error — for example, when testing error conditions or working with intentionally untyped third-party code.
// @ts-check // @ts-ignore suppresses the next line's error (use sparingly) // @ts-ignore const result = someUntypedFunction() // @ts-expect-error is better — it REQUIRES an error to exist. // If the error is later fixed, TypeScript will warn you to remove the comment. // @ts-expect-error — intentionally passing wrong type to test error handling processUser(null)
@ts-expect-error over @ts-ignore. If the code is later fixed and the error disappears, @ts-ignore silently becomes dead weight. @ts-expect-error will flag itself as unnecessary so you can remove it.@satisfies in JSDoc
The @satisfies operator (TypeScript 4.9) validates a value against a type while keeping the narrower inferred type. In JSDoc, use it via a type cast expression.
// @ts-check
/** @typedef {'red' | 'green' | 'blue'} Color */
/** @typedef {Record<string, Color | [number, number, number]>} ColorMap */
// JSDoc does not have a native @satisfies tag, but you can simulate
// it using an inline type assertion cast:
const palette = /** @type {ColorMap} */ ({
primary: 'red',
secondary: [0, 128, 255],
})
// In TypeScript .ts files you would write:
// const palette = { ... } satisfies ColorMap
// Alternatively, assign to a typed variable (this validates the shape):
/** @type {ColorMap} */
const palette2 = {
primary: 'red',
secondary: [0, 128, 255],
}@overload
@overload lets you declare multiple call signatures for a JSDoc-annotated function, just like TypeScript function overloads.
// @ts-check
/**
* @overload
* @param {string} value
* @returns {string}
*//**
* @overload
* @param {number} value
* @returns {number}
*//**
* @param {string | number} value
* @returns {string | number}
*/
function double(value) {
if (typeof value === 'string') {
return value + value
}
return value * 2
}
const s = double('abc') // string
const n = double(5) // number
// const x = double(true) // Error: boolean not assignableVS Code IntelliSense with JSDoc
VS Code uses TypeScript's language server to power IntelliSense in JavaScript files. JSDoc annotations feed directly into this system, giving you:
Hover-over type information on variables and function calls
Autocompletion for object properties and function arguments
Rename refactoring that tracks all usages
Go to definition for types defined in .d.ts files
Inline error squiggles for type mismatches
Parameter hints showing expected types while you type
// @ts-check
/**
* @typedef {Object} Product
* @property {string} id
* @property {string} name
* @property {number} price
* @property {string} [category]
*/
/**
* @param {Product[]} products
* @param {number} maxPrice
* @returns {Product[]}
*/
function filterByPrice(products, maxPrice) {
// VS Code autocompletes product.name, product.price, etc.
return products.filter(p => p.price <= maxPrice)
}
// Usage: VS Code shows argument types as you type
filterByPrice(/* hover sees Product[] */, /* hover sees number */)When to Use JSDoc vs .ts Files
Scenario | Recommendation |
|---|---|
New project | Use .ts files — full TypeScript from day one |
Migrating a large JS project incrementally | Add @ts-check + JSDoc to JS files while converting |
Configuration files (vite.config.js, etc.) | JSDoc works well — avoids compilation step |
Library that must ship as plain JS | Author in .ts, compile to .js + .d.ts for consumers |
Quick script or one-off tool | JSDoc is lighter than setting up a full TS build |
Team unfamiliar with TypeScript | JSDoc is gentler introduction than full .ts migration |
Complex generics and advanced types | Use .ts — JSDoc generic syntax is verbose and limiting |
Complete JSDoc Example
Here is a complete .js module with full JSDoc types — equivalent to what you would write in TypeScript.
// @ts-check
/**
* @typedef {Object} User
* @property {string} id
* @property {string} name
* @property {string} email
* @property {'admin'|'user'} role
* @property {Date} createdAt
*/
/**
* @typedef {Object} CreateUserInput
* @property {string} name
* @property {string} email
* @property {'admin'|'user'} [role]
*/
/** @type {User[]} */
const users = []
/**
* Creates a new user.
*
* @param {CreateUserInput} input
* @returns {User}
*/
function createUser(input) {
/** @type {User} */
const user = {
id: Math.random().toString(36).slice(2),
name: input.name,
email: input.email,
role: input.role ?? 'user',
createdAt: new Date(),
}
users.push(user)
return user
}
/**
* Finds a user by id.
*
* @param {string} id
* @returns {User | undefined}
*/
function findUser(id) {
return users.find(u => u.id === id)
}
/**
* Updates a user's role.
*
* @param {string} id
* @param {'admin'|'user'} role
* @returns {User | null}
*/
function setUserRole(id, role) {
const user = findUser(id)
if (!user) return null
user.role = role
return user
}
module.exports = { createUser, findUser, setUserRole }.ts, want a lighter migration path, or need to ship JS that is still fully typed for consumers.