如何用JSDoc标注条件返回自身参数的函数类型?
问题:JSDoc无法识别基于参数的条件返回类型,导致类型推断失效
问题场景
编写一个根据条件返回传入参数或自定义Page类型的函数时,遇到JSDoc类型标注的两个核心问题:
- 尝试用
{index|user|Page}作为返回类型时,JSDoc无法识别变量指代的具体类型,返回值被判定为any; - 使用
{string|number|Page}作为返回类型后,后续代码中变量仍被识别为联合类型,无法获得Page类型的代码补全和属性提示,只能通过手动添加类型检查缩小范围,但希望找到更优的处理方案。
相关代码示例
函数实现及JSDoc标注:
/** * Finds the page with given index and returns * - index if it does not exist * - user if user doesn't have permissions to read * - else the page itself * * @param {number} index * @param {string} user * @returns {index|user|Page} */ function getRead(index, user) { let page = pages.find(page => page.details.index == index); if (page && page.canRead(user)) { return page; } else if (!page) return index; else return user; }
后续判断代码(类型推断失效):
if(page == index) return res.send(`Page ${index} does not exist`); if(page == user) return res.send(`No permissions to view page ${index}`); // page仍被识别为 number | string | Page,无Page类型的代码补全
添加类型检查后的代码(类型缩小有效,但不够优雅):
if(typeof page == "number" || page == index) return res.send(`Page ${index} does not exist`); if(typeof page == "string" || page == user) return res.send(`No permissions to view page ${index}`); // page被识别为Page类型,可获得属性提示
解决方案
1. 使用TypeScript兼容的JSDoc模板参数(关联输入输出类型)
大部分现代编辑器(如VS Code)支持TypeScript扩展的JSDoc语法,可通过@template标记将输入参数的类型与返回值绑定,让编辑器更精准地推断类型:
/** * @template {number} TIndex * @template {string} TUser * Finds the page with given index and returns * - index if it does not exist * - user if user doesn't have permissions to read * - else the page itself * * @param {TIndex} index * @param {TUser} user * @returns {TIndex | TUser | Page} */ function getRead(index, user) { let page = pages.find(page => page.details.index == index); if (page && page.canRead(user)) { return page; } else if (!page) return index; else return user; }
使用时配合严格相等判断(===)或自定义类型守卫,能让编辑器快速缩小类型范围。
2. 重构为结构化返回值(推荐方案)
避免返回不同类型的原始值/对象,改用带类型标记的结构化对象,从根源上提升类型安全性和代码可读性:
/** * @typedef {Object} GetReadNotFound * @property {'not-found'} type - 页面不存在的状态标记 * @property {number} index - 传入的页面索引 */ /** * @typedef {Object} GetReadPermissionDenied * @property {'permission-denied'} type - 无权限的状态标记 * @property {string} user - 传入的用户标识 */ /** * @typedef {Object} GetReadSuccess * @property {'success'} type - 获取成功的状态标记 * @property {Page} page - 目标页面对象 */ /** * Finds the page with given index and returns a structured result * * @param {number} index * @param {string} user * @returns {GetReadNotFound | GetReadPermissionDenied | GetReadSuccess} */ function getRead(index, user) { let page = pages.find(page => page.details.index == index); if (page && page.canRead(user)) { return { type: 'success', page }; } else if (!page) { return { type: 'not-found', index }; } else { return { type: 'permission-denied', user }; } }
后续使用时,通过switch判断类型标记,编辑器会自动推断对应分支的类型:
const result = getRead(1, 'user123'); switch (result.type) { case 'not-found': res.send(`Page ${result.index} does not exist`); break; case 'permission-denied': res.send(`No permissions to view page ${index}`); break; case 'success': // 此处result.page自动识别为Page类型,可获得完整代码补全 console.log(result.page.details.title); break; }
3. 优化类型守卫逻辑
如果无法重构函数,可通过更精准的类型检查或自定义类型守卫函数来缩小类型范围:
// 自定义类型守卫:判断是否为Page类型 /** * @param {any} value * @returns {value is Page} */ function isPage(value) { return typeof value === 'object' && value !== null && 'canRead' in value && typeof value.canRead === 'function' && 'details' in value; } // 使用示例 const page = getRead(1, 'user123'); if (isPage(page)) { // page被识别为Page类型,支持属性补全 page.details.index; } else if (page === index) { res.send(`Page ${index} does not exist`); } else if (page === user) { res.send(`No permissions to view page ${index}`); }
内容的提问来源于stack exchange,提问作者Fiodar Shurankou
相关产品推荐
相关产品推荐

