# Method Shorthand Jsdoc

> Method shorthand in return objects for JSDoc preservation. Use when factory functions have internal helpers that should expose docs, or hovering over returned methods shows no JSDoc.

- Skill: `epicenterhq/method-shorthand-jsdoc` (Agent Skill)
- Install (CLI): `npx skillmds@latest add epicenterhq/method-shorthand-jsdoc`
- Raw SKILL.md: https://api.skillmd.com/api/skills/epicenterhq/method-shorthand-jsdoc/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: epicenterhq (https://skillmd.com/u/epicenterhq)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/epicenterhq/method-shorthand-jsdoc

---


# Method Shorthand for JSDoc Preservation

When factory functions have helper functions that are only used by returned methods, move them INTO the return object using method shorthand. This ensures JSDoc comments are properly passed through to consumers.

> **Related Skills**: See `factory-function-composition` for the four-zone factory anatomy and the `this` decision rule.

## The Problem

You write a factory function with a well-documented helper:

```typescript
function createHeadDoc(options: { workspaceId: string }) {
	const { workspaceId } = options;

	/**
	 * Get the current epoch number.
	 *
	 * Computes the maximum of all client-proposed epochs.
	 * This ensures concurrent bumps converge to the same version.
	 *
	 * @returns The current epoch (0 if no bumps have occurred)
	 */
	function getEpoch(): number {
		let max = 0;
		for (const value of epochsMap.values()) {
			max = Math.max(max, value);
		}
		return max;
	}

	return {
		workspaceId,
		getEpoch, // JSDoc is NOT visible when hovering on returned object!

		bumpEpoch(): number {
			const next = getEpoch() + 1; // Calling internal helper
			return next;
		},
	};
}
```

When you hover over `head.getEpoch()` in your IDE, you see... nothing. The JSDoc is lost.

## The Solution

Move the helper INTO the return object using method shorthand:

```typescript
function createHeadDoc(options: { workspaceId: string }) {
	const { workspaceId } = options;

	return {
		workspaceId,

		/**
		 * Get the current epoch number.
		 *
		 * Computes the maximum of all client-proposed epochs.
		 * This ensures concurrent bumps converge to the same version.
		 *
		 * @returns The current epoch (0 if no bumps have occurred)
		 */
		getEpoch(): number {
			let max = 0;
			for (const value of epochsMap.values()) {
				max = Math.max(max, value);
			}
			return max;
		},

		bumpEpoch(): number {
			const next = this.getEpoch() + 1; // Use this.methodName()
			return next;
		},
	};
}
```

Now hovering over `head.getEpoch()` shows the full JSDoc.

This matters even more when the public type derives from the factory:

```typescript
export type HeadDoc = ReturnType<typeof createHeadDoc>;
```

With `ReturnType`, the returned object is the public type source. Put consumer-facing JSDoc directly on the returned method or getter so hover, completion, and Go to Definition all land on the same member.

JSDoc preservation and Go-to-Definition flow from the same choice: where the consumer's tools resolve to. Method shorthand in zone 4 keeps both on the real definition. A hand-written interface annotation, a destructure-re-export of a module-level object, or a wrapper that just re-emits a function are the regressions that split them apart. See `typescript` "Go-to-Definition Awareness" for the navigation-side rules.

## Why This Works

1. **JSDoc attaches to the method definition site** - when methods are inline in the return object, the JSDoc is directly on the property TypeScript sees
2. **Method shorthand uses `function` semantics** - `this` is bound to the object, so `this.getEpoch()` works
3. **No separate helper needed** - if it's only used by sibling methods, it belongs in the same object

## The Pattern

```typescript
// BAD: Helper defined separately, JSDoc lost on return
function createService(client) {
  /** Fetches user data with caching. */
  function fetchUser(id: string) { ... }

  return {
    fetchUser,  // JSDoc not visible to consumers!
    getProfile(id: string) {
      return fetchUser(id);  // Works, but consumers can't see docs
    },
  };
}

// GOOD: Method shorthand, JSDoc preserved
function createService(client) {
  return {
    /** Fetches user data with caching. */
    fetchUser(id: string) { ... },

    getProfile(id: string) {
      return this.fetchUser(id);  // Use this.method()
    },
  };
}
```

## Decision Rule

Move a helper into the returned object when it is only used by returned methods, consumers need hover JSDoc on that method, and the helper does not run during initialization.

Keep helpers separate when they are called before `return`, shared across factories, or truly internal and not exposed.

## Arrow Functions Don't Work

Arrow functions don't have their own `this`:

```typescript
// BAD: Arrow function, this is undefined
return {
  getEpoch: () => { ... },
  bumpEpoch: () => {
    this.getEpoch();  // ERROR: this is undefined!
  },
};

// GOOD: Method shorthand has correct this binding
return {
  getEpoch() { ... },
  bumpEpoch() {
    this.getEpoch();  // Works!
  },
};
```

## Real Example

From `packages/epicenter/src/core/docs/head-doc.ts`:

```typescript
export function createHeadDoc(options: { workspaceId: string; ydoc?: Y.Doc }) {
	const { workspaceId } = options;
	const ydoc = options.ydoc ?? new Y.Doc({ guid: workspaceId });
	const epochsMap = ydoc.getMap<number>('epochs');

	return {
		ydoc,
		workspaceId,

		/**
		 * Get the current epoch number.
		 *
		 * Computes the maximum of all client-proposed epochs.
		 * This ensures concurrent bumps converge to the same version
		 * without skipping epoch numbers.
		 *
		 * @returns The current epoch (0 if no bumps have occurred)
		 */
		getEpoch(): number {
			let max = 0;
			for (const value of epochsMap.values()) {
				max = Math.max(max, value);
			}
			return max;
		},

		/**
		 * Bump the epoch to the next version.
		 *
		 * @returns The new epoch number after bumping
		 */
		bumpEpoch(): number {
			const next = this.getEpoch() + 1;
			epochsMap.set(ydoc.clientID.toString(), next);
			return next;
		},

		// ... other methods using this.getEpoch()
	};
}
```

## Summary

| Approach                    | JSDoc Visible? | `this` Works? |
| --------------------------- | -------------- | ------------- |
| Separate helper + reference | No             | N/A           |
| Arrow function in return    | Yes            | No            |
| Method shorthand in return  | Yes            | Yes           |

Method shorthand is the only approach that preserves JSDoc AND allows methods to call each other via `this`.

## Where This Fits in the Factory Function Anatomy

Factory functions follow a four-zone internal shape: immutable state → mutable state → private helpers → return object. Method shorthand lives in the return object (zone 4): the public API.

The `this.method()` vs direct-call decision depends on which zone the function lives in:

| Situation | Where it lives | How to call it |
|---|---|---|
| Only used by sibling methods in the return object | Zone 4 (return object, method shorthand) | `this.method()` |
| Used by return-object methods AND pre-return init logic | Zone 3 (private helper, standalone function) | Direct call: `helperFn()` |
| Used during initialization only, not exposed | Zone 3 (private helper) | Direct call: `helperFn()` |

When a helper needs to be in zone 3, its JSDoc won't be visible to consumers; that's correct, because it's a private implementation detail. Only zone 4 methods need consumer-facing JSDoc.

See [Closures Are Better Privacy Than Keywords](../../../docs/articles/closures-are-better-privacy-than-keywords.md) for the full factory function anatomy.

## References

- [docs/articles/method-shorthand-jsdoc-preservation.md](../../../docs/articles/method-shorthand-jsdoc-preservation.md) - Same content as article
- [docs/articles/closures-are-better-privacy-than-keywords.md](../../../docs/articles/closures-are-better-privacy-than-keywords.md) - Factory function anatomy and zone system

