关于 method-shorthand-jsdoc
这项技能介绍如何组织 TypeScript 工厂函数,让返回方法上的 JSDoc 对使用者可见。重点是将仅供返回对象方法使用的辅助函数写成对象内的方法简写。
主要功能
- 01保留返回方法上的 JSDoc
- 02在返回对象中使用方法简写
- 03指导辅助函数的放置位置
- 04说明用 this 调用同级方法
适用场景
- 01设计 TypeScript 工厂函数 API
- 02让 IDE 悬停文档保持可见
- 03选择私有辅助函数或对象方法
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ epicenterhq/ epicenter --skill method-shorthand-jsdoc安装后在 Claude Code 中输入/method-shorthand-jsdoc确认已加载。
详细介绍
保留 JSDoc 的方法简写
当工厂函数中的辅助函数仅供返回的方法使用时,将它们移入返回对象,并使用方法简写。这样可以确保 JSDoc 注释正确传递给使用者。
相关技能:参阅
factory-function-composition,了解工厂函数的四区结构以及this的判断规则。
问题
你编写了一个带有详细文档的辅助函数的工厂函数:
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;
},
};
}
在 IDE 中将鼠标悬停在 head.getEpoch() 上时,你会看到……什么都没有。JSDoc 丢失了。
解决方案
将辅助函数移入返回对象,并使用方法简写:
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;
},
};
}
现在,将鼠标悬停在 head.getEpoch() 上时,就会显示完整的 JSDoc。
当公共类型由工厂函数推导时,这一点尤为重要:
export type HeadDoc = ReturnType<typeof createHeadDoc>;
使用 ReturnType 时,返回对象就是公共类型的来源。将面向使用者的 JSDoc 直接写在返回的方法或 getter 上,这样悬停提示、自动补全和“转到定义”都会指向同一个成员。
JSDoc 保留和“转到定义”的行为取决于同一个选择:使用者的工具会解析到哪里。第 4 区中的方法简写能让两者都指向真实定义。手写的接口注解、对模块级对象进行解构并重新导出,或仅重新导出函数的包装器,都会导致两者分离。请参阅 typescript 中的“转到定义注意事项”,了解导航方面的规则。
原理
- JSDoc 会附加到方法定义处——方法直接内联在返回对象中时,JSDoc 就位于 TypeScript 所识别的属性上
- 方法简写采用
function语义——this会绑定到对象,因此this.getEpoch()可以正常工作 - 无需单独的辅助函数——如果它只由同级方法使用,就应该放在同一个对象中
模式
// 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()
},
};
}
判断规则
当辅助函数仅供返回的方法使用、使用者需要在该方法上通过悬停查看 JSDoc,并且辅助函数不会在初始化期间运行时,将其移入返回对象。
如果辅助函数在 return 之前调用、由多个工厂函数共用,或者确实是未公开的内部实现,则应将其单独保留。
箭头函数不适用
箭头函数没有自己的 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!
},
};
实际示例
摘自 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()
};
}
总结
| 方式 | JSDoc 可见? | this 可用? |
|---|---|---|
| 单独的辅助函数 + 引用 | 否 | 不适用 |
| 返回对象中的箭头函数 | 是 | 否 |
| 返回对象中的方法简写 | 是 | 是 |
方法简写是唯一既能保留 JSDoc,又能让方法通过 this 相互调用的方式。
在工厂函数四区结构中的位置
工厂函数遵循四区内部结构:不可变状态 → 可变状态 → 私有辅助函数 → 返回对象。方法简写位于返回对象(第 4 区):也就是公共 API。
使用 this.method() 还是直接调用,取决于函数所在的区域:
| 情况 | 所在位置 | 调用方式 |
|---|---|---|
| 仅由返回对象中的同级方法使用 | 第 4 区(返回对象,方法简写) | this.method() |
由返回对象中的方法和 return 之前的初始化逻辑共同使用 | 第 3 区(私有辅助函数,独立函数) | 直接调用:helperFn() |
| 仅在初始化期间使用,不对外公开 | 第 3 区(私有辅助函数) | 直接调用:helperFn() |
当辅助函数必须位于第 3 区时,它的 JSDoc 不会对使用者可见;这是正确的,因为它属于私有实现细节。只有第 4 区的方法才需要面向使用者的 JSDoc。
有关工厂函数完整的四区结构,请参阅闭包比关键字更适合实现隐私。