Move helper functions into return objects using method shorthand for proper JSDoc preservation. Use when factory functions have internal helpers that should expose documentation to consumers, or when hovering over returned methods shows no JSDoc.
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-compositionfor the four-zone factory anatomy and thethisdecision rule.
You write a factory function with a well-documented helper:
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.
Move the helper INTO the return object using method shorthand:
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.
function semantics - this is bound to the object, so this.getEpoch() works// 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()
},
};
}
Use this pattern when:
Keep helpers separate when:
Arrow functions don't have their own this:
// 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!
},
};
From packages/epicenter/src/core/docs/head-doc.ts:
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()
};
}
| 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.
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 for the full factory function anatomy.