# JSDoc: The TypeScript Experience Without TypeScript

Remember that time you called a function, forgot what parameters it needed, then had to scroll up 200 lines to check? Or when you refactored a function and broke 12 files because nothing told you where it was being used incorrectly?

JSDoc fixes this. It gives you TypeScript-level autocomplete, type checking, and documentation — all in plain JavaScript. No build step, no configuration, just comments.

## The Problem with Undocumented Code

Here's a function without documentation:

```javascript
function calculatePrice(item, quantity, discount) {
  const subtotal = item.price * quantity;
  const discountAmount = subtotal * (discount / 100);
  return subtotal - discountAmount;
}
```

When you call this function three months later, you have questions: Is `discount` a percentage (15) or a decimal (0.15)? Is `item` an object or just the price? What shape does it have?

Here's the same function with JSDoc:

```javascript
/**
 * Calculate final price after discount
 * @param {Object} item - The product item
 * @param {number} item.price - Unit price
 * @param {string} item.name - Product name
 * @param {number} quantity - Number of items
 * @param {number} discount - Discount percentage (0-100)
 * @returns {number} Final price after discount
 */
function calculatePrice(item, quantity, discount) {
  const subtotal = item.price * quantity;
  const discountAmount = subtotal * (discount / 100);
  return subtotal - discountAmount;
}
```

Now when you type `calculatePrice(`, your editor shows you exactly what each parameter needs. No guessing. No context switching.

![](https://cdn.hashnode.com/uploads/covers/69d007f5e466e2b7625cd1df/d68e2140-df11-4f08-b60a-0c4c0892d8e0.png align="center")

## Custom Types with @typedef

For objects you use frequently, define them once and reuse everywhere:

```javascript
/**
 * @typedef {Object} User
 * @property {string} id - User ID
 * @property {string} email - User email
 * @property {string} name - User name
 * @property {string[]} roles - User roles
 * @property {Date} createdAt - Account creation date
 */

/**
 * @typedef {Object} AuthToken
 * @property {string} token - JWT token
 * @property {Date} expiresAt - Token expiration
 */

/**
 * Authenticate user and return token
 * @param {string} email - User's email
 * @param {string} password - User's password
 * @returns {Promise<{user: User, auth: AuthToken}>}
 */
async function login(email, password) {
  const user = await User.findOne({ email });
  if (!user) throw new Error('User not found');
  
  const valid = await bcrypt.compare(password, user.password);
  if (!valid) throw new Error('Invalid password');
  
  const token = jwt.sign({ userId: user.id }, SECRET);
  
  return {
    user: {
      id: user.id,
      email: user.email,
      name: user.name,
      roles: user.roles,
      createdAt: user.createdAt
    },
    auth: {
      token,
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
    }
  };
}
```

When you call `login()`, your editor knows the returned object has `user` and `auth` properties with their full structure. Type `user.` and see all five properties. Type `auth.` and see token and expiration.

## Importing Types from Libraries

You don't have to define everything yourself. Import types from npm packages:

```javascript
/**
 * @typedef {import('express').Request} Request
 * @typedef {import('express').Response} Response
 * @typedef {import('express').NextFunction} NextFunction
 */

/**
 * Get user by ID
 * @param {Request} req - Express request
 * @param {Response} res - Express response
 * @returns {Promise<void>}
 */
async function getUser(req, res) {
  const userId = req.params.id;
  const user = await User.findById(userId);
  
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  res.json(user);
}
```

Type `req.` and your editor suggests `params`, `query`, `body`, `headers` — everything Express provides. Same with `res.status()`, `res.json()`, `res.send()`.

## Type Checking with @ts-check

Add one comment at the top of your file and get TypeScript error checking:

```javascript
// @ts-check

/**
 * @param {number} a
 * @param {number} b
 * @returns {number}
 */
function add(a, b) {
  return a + b;
}

const result = add(5, '10'); // ❌ Error: Argument of type 'string' is not assignable to parameter of type 'number'
```

Your editor warns you about type mismatches before you run the code. No TypeScript compiler needed.

This catches bugs at write-time:

```javascript
// @ts-check

/**
 * @typedef {Object} Product
 * @property {string} id
 * @property {string} name
 * @property {number} price
 */

/**
 * @param {Product[]} products
 * @returns {number}
 */
function calculateTotal(products) {
  return products.reduce((sum, product) => sum + product.price, 0);
}

const products = [
  { id: '1', name: 'Book', price: 20 },
  { id: '2', name: 'Pen', price: 5 },
  { id: '3', name: 'Notebook', price: '15' } // ❌ Error: Type 'string' is not assignable to type 'number'
];

calculateTotal(products);
```

The error shows up immediately in your editor. Not in production. Not in testing. Right here.

## Enums and Literal Types

Document allowed values to prevent invalid inputs:

```javascript
/**
 * @typedef {'pending' | 'active' | 'completed' | 'cancelled'} OrderStatus
 */

/**
 * Update order status
 * @param {string} orderId - Order ID
 * @param {OrderStatus} status - New status
 * @returns {Promise<void>}
 */
async function updateOrderStatus(orderId, status) {
  await Order.updateOne(
    { _id: orderId },
    { status }
  );
}

// Editor suggests: 'pending', 'active', 'completed', 'cancelled'
await updateOrderStatus('order-123', 'active');

// Editor warns about invalid value
await updateOrderStatus('order-123', 'shipped'); // ❌ Not a valid OrderStatus
```

## Class Documentation

JSDoc works great with ES6 classes:

```javascript
/**
 * User service for managing user accounts
 */
class UserService {
  /**
   * @param {Object} database - Database connection
   */
  constructor(database) {
    this.db = database;
  }

  /**
   * Find user by email
   * @param {string} email - User email address
   * @returns {Promise<User|null>} User object or null
   */
  async findByEmail(email) {
    return await this.db.users.findOne({ email });
  }

  /**
   * Create new user
   * @param {Object} userData - User data
   * @param {string} userData.email - Email address
   * @param {string} userData.password - Password
   * @param {string} userData.name - Full name
   * @returns {Promise<User>} Created user
   */
  async create(userData) {
    const hashedPassword = await this.hashPassword(userData.password);
    return await this.db.users.create({
      ...userData,
      password: hashedPassword
    });
  }

  /**
   * @private
   * @param {string} password
   * @returns {Promise<string>}
   */
  async hashPassword(password) {
    return await bcrypt.hash(password, 10);
  }
}

const userService = new UserService(db);
const user = await userService.findByEmail('test@example.com');
```

The `@private` tag tells your editor that `hashPassword` is internal. It might not show in autocomplete or might show with a warning.

## Callbacks and Higher-Order Functions

Document function parameters that are themselves functions:

```javascript
/**
 * @callback ValidatorFn
 * @param {any} value - Value to validate
 * @returns {boolean} True if valid
 */

/**
 * @callback TransformFn
 * @param {any} value - Value to transform
 * @returns {any} Transformed value
 */

/**
 * Process array with validation and transformation
 * @param {any[]} items - Array to process
 * @param {ValidatorFn} validate - Validation function
 * @param {TransformFn} transform - Transform function
 * @returns {any[]} Processed array
 */
function processArray(items, validate, transform) {
  return items
    .filter(item => validate(item))
    .map(item => transform(item));
}

const numbers = [1, 2, 3, 4, 5];
const result = processArray(
  numbers,
  (num) => num > 2,     // Editor knows this should return boolean
  (num) => num * 2      // Editor knows this can return any type
);
```

## Real-World Express Example

Here's how JSDoc makes Express development better:

```javascript
// @ts-check
/**
 * @typedef {import('express').Request} Request
 * @typedef {import('express').Response} Response
 * @typedef {import('express').NextFunction} NextFunction
 */

/**
 * @typedef {Object} AuthUser
 * @property {string} id - User ID
 * @property {string[]} roles - User roles
 */

/**
 * @typedef {Request & {user: AuthUser}} AuthRequest
 */

/**
 * Authenticate request using JWT
 * @param {Request} req
 * @param {Response} res
 * @param {NextFunction} next
 */
function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }

  try {
    const decoded = jwt.verify(token, SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
}

/**
 * Get current user profile
 * @param {AuthRequest} req
 * @param {Response} res
 */
async function getProfile(req, res) {
  const user = await User.findById(req.user.id);
  res.json(user);
}
```

When you type `req.user.`, your editor suggests `id` and `roles`. When you type `res.`, it suggests all Express response methods.

## Common JSDoc Tags Reference

Here are the tags you'll use most:

```javascript
/**
 * @param {Type} name - Description
 * @returns {Type} Description
 * @throws {Error} Description
 * @typedef {Object} TypeName
 * @property {Type} name - Description
 * @callback CallbackName
 * @template T - Generic type parameter
 * @example
 * functionName(arg1, arg2)
 * @deprecated Use newFunction() instead
 * @see {@link OtherFunction}
 * @private
 * @async
 */
```

## When Not to Use JSDoc

JSDoc isn't always the answer:

**Don't use JSDoc when:**

*   Your team is already using TypeScript (just use TypeScript)
    
*   The function is so simple that docs add no value
    
*   You're documenting implementation details instead of the API
    

**Do use JSDoc when:**

*   You're in a JavaScript codebase and want better developer experience
    
*   You need type safety but can't add a build step
    
*   You're maintaining a library and want to help users
    
*   You want autocomplete without migrating to TypeScript
    

## Making It a Habit

The best way to start:

1.  Add `// @ts-check` to the top of new files
    
2.  Document function signatures as you write them
    
3.  Define `@typedef` for objects you use frequently
    
4.  Import types from `@types` packages for libraries you use
    

Type `/**` above any function and hit Enter. Most editors auto-generate a JSDoc template. Fill in the types. Done.

Five extra seconds per function. Saves hours of debugging later.

## The Documentation Bonus

JSDoc comments can generate actual documentation sites. Tools like `jsdoc` or `documentation.js` turn your comments into HTML docs:

```bash
npm install -g jsdoc
jsdoc src/**/*.js -d docs
```

Your inline comments become browsable documentation. One source of truth for both developers and docs.

* * *

## TLDR;

JSDoc gives you TypeScript-level autocomplete, type checking, and documentation in plain JavaScript. Add `/** */` comments above functions with `@param`, `@returns`, and `@typedef` tags. Your editor parses these and provides autocomplete, parameter hints, and error checking. Use `// @ts-check` at the top of files to enable type validation. Import types from npm packages with `@typedef {import('package').Type}`. No build step, no configuration — just better developer experience. Five seconds to document a function, hours saved in debugging and onboarding.
