method-shorthand-jsdoc

$npx mdskill add EpicenterHQ/epicenter/method-shorthand-jsdoc

Preserves JSDoc for factory function return methods using method shorthand

  • Solves missing JSDoc visibility in IDEs for returned factory methods
  • Relies on JavaScript method shorthand syntax and JSDoc comment placement
  • Analyzes factory structure to identify internal helper functions needing exposure
  • Moves helpers into return object to maintain documentation linkage

SKILL.md

.github/skills/method-shorthand-jsdocView on GitHub ↗
---
name: method-shorthand-jsdoc
description: 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.
metadata:
  author: epicenter
  version: '1.0'
---

# 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.

## 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()
    },
  };
}
```

## When to Apply

Use this pattern when:

- Helper functions are ONLY used by methods in the return object
- You want JSDoc visible when consumers hover over the method
- The helper doesn't need to be called before the return statement

Keep helpers separate when:

- They're called during initialization (before return)
- They're used by multiple factories (extract to shared module)
- They're truly internal and shouldn't be 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—but 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

More from EpicenterHQ/epicenter

SkillDescription
agent-goalWrite `/goal` prompts for long-running agent work in Codex or Claude Code. Use for slash goal, agent goal, durable objective, autonomous coding run.
approachability-auditReview code as a new TypeScript developer. Use when code feels indirect, clever, hard to follow, or needs a pass on abstractions, names, first-read clarity.
arktypeArktype: runtime validation, discriminated unions with .merge()/.or(), spread keys. Use when mentioning arktype, type(), union types, command/event schemas.
attach-primitiveContract and invariants for `attach*` composition primitives in `packages/workspace` (side-effectful building blocks like attachIndexedDb, attachSqlite, attachBroadcastChannel, attachEncryption, attachTable, openCollaboration), and when to use `create*` (pure construction) instead. Use when writing or reviewing an `attach*` or `create*` function, naming a new workspace primitive, composing inside a workspace builder, or deciding whether a primitive registers listeners at call time.
authEpicenter auth packages: `@epicenter/auth`, `@epicenter/auth-svelte`, OAuth sessions, identity state, auth-owned fetch/WebSocket, and workspace lifecycle binding. Use when editing Epicenter auth clients, session state, hosted sign-in, or auth/workspace integration.
autumnAutumn billing in Epicenter: `autumn.config.ts`, `autumn-js` credit checks, `atmn` CLI, plan gates, and metered AI usage. Use when changing billing, pricing, credits, plan access, refunds, or usage events.
better-auth-best-practicesBetter Auth server/client setup: `auth.ts`, generated schema, DB adapters, sessions, cookies, env vars, and plugins. Use when mentioning Better Auth, betterauth, auth handlers, OAuth, email/password, or session configuration.
better-auth-security-best-practicesBetter Auth security hardening: rate limits, secrets, CSRF, trusted origins, cookies, sessions, OAuth tokens, and audit logging. Use when reviewing auth security, brute-force protection, token handling, or deployment safety.
change-proposalPresent proposed code changes visually before implementing. Use when: "show me options", "compare approaches", "what should we do", or when changes need before/after comparison.
claude-code-consultUse this skill when the user asks to consult Claude, ask Claude Code, get another model's take, run a taste check, find cleaner options, or prepare a Claude prompt. Create a bounded second-opinion prompt or run a read-only Claude Code consult, then verify Claude's claims against local files.