从基础到进阶,覆盖90%日常开发场景
前言:从一个真实的案例开始
最近Review团队代码时,看到一个典型的"遗留代码困境":
// 一个真实的业务函数(已简化)
function processOrder(data, config, callback) {
// 200行业务逻辑
// data是什么结构?config有哪些选项?callback要传什么参数?
// 不看函数实现完全不知道
}
当我问作者"这个函数怎么用时",他挠了挠头:"让我看看啊...哦,data应该是订单对象,config有这几个属性..."
这就是没有类型注释的代价:即使是你自己写的代码,一个月后也会忘记接口约定。
今天,我分享10个JSDoc实战技巧,帮你从"猜谜代码"转向"明确契约"。每个技巧都有真实场景和具体代码,可以直接复制使用。
技巧1:从"模糊对象"到"明确契约"(基础必备)
场景:处理API返回的不确定结构数据
// ❌ 模糊写法 - 知道是个对象,但不知道具体结构
function handleResponse(response) {
console.log(response.data.user.name);
// 如果response结构变化,这里会默默失败
}
// ✅ 明确写法 - 定义清晰的数据契约
/**
* API用户数据响应
* @typedef {Object} UserApiResponse
* @property {boolean} success - 请求是否成功
* @property {Object} data - 响应数据
* @property {Object} data.user - 用户数据
* @property {string} data.user.id - 用户ID
* @property {string} data.user.name - 用户名
* @property {string} data.user.email - 邮箱
* @property {number} [data.user.age] - 年龄(可选)
*/
/**
* 处理用户数据响应
* @param {UserApiResponse} response - API响应
*/
function handleResponse(response) {
// 现在有完整的智能提示
if (response.success) {
console.log(`用户 ${response.data.user.name} 的邮箱是 ${response.data.user.email}`);
// 如果尝试访问不存在的属性,IDE会立即警告
// console.log(response.data.user.phone); // ❌ Property 'phone' does not exist
}
}
核心价值:把隐式约定变成显式契约,预防运行时错误。
技巧2:可选参数的"正确打开方式"
场景:配置对象中有大量可选参数
// ❌ 模糊的可选参数
function createUser(options) {
const name = options.name || '匿名';
const age = options.age || 18;
// 问题:options还可能有哪些属性?默认值是什么?
}
// ✅ 明确的可选参数配置
/**
* 用户创建选项
* @typedef {Object} CreateUserOptions
* @property {string} [name='匿名'] - 用户名(默认:匿名)
* @property {number} [age=18] - 年龄(默认:18)
* @property {'male'|'female'|'other'} [gender='male'] - 性别(默认:male)
* @property {string} [email] - 邮箱(可选)
* @property {boolean} [isActive=true] - 是否激活(默认:true)
* @property {string[]} [tags=[]] - 标签(默认:空数组)
*/
/**
* 创建用户
* @param {CreateUserOptions} options - 用户选项
* @returns {Object} 创建的用户对象
*/
function createUser(options = {}) {
// 使用解构赋默认值,清晰明了
const {
name = '匿名',
age = 18,
gender = 'male',
email,
isActive = true,
tags = []
} = options;
return {
id: generateId(),
name,
age,
gender,
email,
isActive,
tags,
createdAt: new Date()
};
}
// 调用时有完整提示
const user = createUser({
name: '张三',
age: 25,
// gender: 'female', // 可选,有默认值
// email: 'zhangsan@example.com', // 可选,无默认值
// isActive: false, // 可选,有默认值
// tags: ['vip', 'new'] // 可选,有默认值
});
核心价值:明确每个可选参数的默认值和含义,避免配置混淆。
技巧3:处理"要么A要么B"的联合类型
场景:函数参数可以是不同类型或不同格式
// ❌ 含糊的类型判断
function parseInput(input) {
if (typeof input === 'string') {
return JSON.parse(input);
} else if (Array.isArray(input)) {
return input;
}
// 还有哪些情况?
}
// ✅ 明确的联合类型
/**
* 解析用户输入
* @param {string | Array<number> | { data: Array<number> }} input - 输入数据
* @returns {Array<number>} 数字数组
* @throws {Error} 当输入格式无法解析时
*/
function parseInput(input) {
if (typeof input === 'string') {
try {
const parsed = JSON.parse(input);
// 类型守卫:确保解析后是数组
if (Array.isArray(parsed)) {
return parsed.filter(item => typeof item === 'number');
}
throw new Error('字符串必须是JSON数组格式');
} catch (error) {
throw new Error(`JSON解析失败: ${error.message}`);
}
}
if (Array.isArray(input)) {
return input.filter(item => typeof item === 'number');
}
if (input && Array.isArray(input.data)) {
return input.data.filter(item => typeof item === 'number');
}
throw new Error('输入格式不支持');
}
// 调用示例 - 所有情况都有明确处理
const result1 = parseInput('[1, 2, 3]'); // ✅ 字符串
const result2 = parseInput([1, 2, 3]); // ✅ 数组
const result3 = parseInput({ data: [1, 2, 3] }); // ✅ 对象
// const result4 = parseInput(123); // ❌ 类型错误,IDE会提示
核心价值:明确所有可能的输入类型,避免遗漏情况。
技巧4:函数重载的优雅实现
场景:同一个函数支持多种参数组合
// ❌ 冗长的参数判断
function createElement(tag, className, content) {
// 根据参数数量判断逻辑...
}
// ✅ 使用@overload明确不同调用方式
/**
* 创建DOM元素
* @overload
* @param {string} tag - 标签名
* @returns {HTMLElement}
*/
/**
* @overload
* @param {string} tag - 标签名
* @param {string} className - CSS类名
* @returns {HTMLElement}
*/
/**
* @overload
* @param {string} tag - 标签名
* @param {string} className - CSS类名
* @param {string|HTMLElement|Array<HTMLElement>} content - 内容
* @returns {HTMLElement}
*/
/**
* 创建DOM元素
* @param {string} tag - 标签名
* @param {string} [className] - CSS类名
* @param {string|HTMLElement|Array<HTMLElement>} [content] - 内容
* @returns {HTMLElement}
*/
function createElement(tag, className, content) {
const element = document.createElement(tag);
if (className) {
element.className = className;
}
if (content) {
if (typeof content === 'string') {
element.textContent = content;
} else if (Array.isArray(content)) {
element.append(...content);
} else {
element.appendChild(content);
}
}
return element;
}
// 不同的调用方式都有正确的提示
const div1 = createElement('div'); // ✅ 只传标签
const div2 = createElement('div', 'container'); // ✅ 标签+类名
const div3 = createElement('div', 'container', 'Hello'); // ✅ 标签+类名+文本
const div4 = createElement('div', 'container', document.createElement('span')); // ✅ 标签+类名+元素
核心价值:让IDE为不同的调用方式提供准确的提示。
技巧5:异步操作的完整类型定义
场景:处理Promise和async/await操作
// ❌ 不完整的异步类型
async function fetchData(url) {
const response = await fetch(url);
return response.json(); // 返回的是什么类型?
}
// ✅ 完整的异步类型定义
/**
* API响应包装类型
* @template T
* @typedef {Object} ApiResult
* @property {boolean} success - 是否成功
* @property {T} [data] - 数据(成功时)
* @property {string} [message] - 错误信息(失败时)
* @property {number} code - 状态码
*/
/**
* 用户数据类型
* @typedef {Object} UserData
* @property {string} id - 用户ID
* @property {string} name - 用户名
* @property {string} email - 邮箱
* @property {number} score - 积分
*/
/**
* 获取用户数据
* @param {string} userId - 用户ID
* @returns {Promise<ApiResult<UserData>>} API响应结果
* @throws {TypeError} 当userId不是字符串时
* @throws {Error} 当网络请求失败时
*/
async function fetchUserData(userId) {
if (typeof userId !== 'string') {
throw new TypeError('userId必须是字符串');
}
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
/** @type {ApiResult<UserData>} */
const result = await response.json();
return result;
} catch (error) {
// 网络错误或其他异常
console.error('获取用户数据失败:', error);
throw new Error(`获取用户数据失败: ${error.message}`);
}
}
// 使用时的完整类型支持
async function displayUser(userId) {
try {
const result = await fetchUserData(userId);
if (result.success && result.data) {
// result.data是UserData类型,有完整提示
console.log(`用户: ${result.data.name}`);
console.log(`邮箱: ${result.data.email}`);
console.log(`积分: ${result.data.score}`);
} else {
console.error(`获取失败: ${result.message}`);
}
} catch (error) {
console.error('操作失败:', error);
}
}
// 错误的调用方式会被提示
// fetchUserData(123); // ❌ Argument of type 'number' is not assignable to parameter of type 'string'
核心价值:让异步操作的输入输出和异常都有明确的类型定义。
技巧6:泛型工具函数的实战应用
场景:编写可复用的工具函数,支持多种数据类型
// ❌ 类型固定的工具函数
function getFirstItem(items) {
return items[0]; // items是什么类型?返回什么类型?
}
// ✅ 泛型工具函数
/**
* 获取数组的第一个元素
* @template T
* @param {T[]} array - 任意类型的数组
* @returns {T | undefined} 第一个元素或undefined
*/
function getFirstItem(array) {
return array.length > 0 ? array[0] : undefined;
}
/**
* 将对象数组转换为键值映射
* @template T
* @param {T[]} array - 对象数组
* @param {keyof T} keyProp - 作为键的属性名
* @returns {Record<string, T>} 键值映射
* @example
* const users = [{ id: '1', name: '张三' }, { id: '2', name: '李四' }];
* const userMap = arrayToMap(users, 'id');
* // userMap的类型是 Record<string, { id: string, name: string }>
*/
function arrayToMap(array, keyProp) {
/** @type {Record<string, T>} */
const map = {};
for (const item of array) {
const key = item[keyProp];
if (typeof key === 'string' || typeof key === 'number') {
map[String(key)] = item;
}
}
return map;
}
/**
* 深度合并多个对象
* @template T
* @param {...Partial<T>} objects - 要合并的对象
* @returns {T} 合并后的对象
*/
function deepMerge(...objects) {
/** @type {any} */
const result = {};
for (const obj of objects) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'object' && obj[key] !== null &&
typeof result[key] === 'object' && result[key] !== null) {
// 递归合并对象
result[key] = deepMerge(result[key], obj[key]);
} else {
result[key] = obj[key];
}
}
}
}
return /** @type {T} */ (result);
}
// 使用示例
const numbers = [1, 2, 3];
const firstNumber = getFirstItem(numbers); // number | undefined
const strings = ['a', 'b', 'c'];
const firstString = getFirstItem(strings); // string | undefined
const users = [
{ id: '1', name: '张三', age: 25 },
{ id: '2', name: '李四', age: 30 }
];
const userMap = arrayToMap(users, 'id');
// userMap['1'] 的类型是 { id: string, name: string, age: number }
console.log(userMap['1'].name); // ✅ 正确的类型提示
const merged = deepMerge(
{ name: '张三', settings: { theme: 'dark' } },
{ age: 25, settings: { fontSize: 14 } }
);
// merged 的类型是 { name: string, age: number, settings: { theme: string, fontSize: number } }
核心价值:编写灵活且类型安全的工具函数,提升代码复用性。
技巧7:DOM操作的类型安全
场景:操作DOM元素时避免常见的类型错误
// ❌ 不安全的DOM操作
function updateElement(elementId, content) {
const element = document.getElementById(elementId);
element.innerHTML = content; // element可能是null!
}
// ✅ 类型安全的DOM操作
/**
* 安全地获取DOM元素
* @template {keyof HTMLElementTagNameMap} K
* @param {string} id - 元素ID
* @param {K} [expectedTag] - 期望的标签名
* @returns {HTMLElementTagNameMap[K] | null}
*/
function getElementSafe(id, expectedTag) {
const element = document.getElementById(id);
if (!element) {
console.warn(`元素 #${id} 不存在`);
return null;
}
if (expectedTag && element.tagName.toLowerCase() !== expectedTag.toLowerCase()) {
console.warn(`元素 #${id} 不是 <${expectedTag}>,而是 <${element.tagName.toLowerCase()}>`);
return null;
}
return /** @type {HTMLElementTagNameMap[K]} */ (element);
}
/**
* 安全地更新元素内容
* @param {string} elementId - 元素ID
* @param {string|HTMLElement} content - 要设置的内容
* @returns {boolean} 是否更新成功
*/
function updateElementSafe(elementId, content) {
const element = getElementSafe(elementId);
if (!element) {
return false;
}
try {
if (typeof content === 'string') {
// 安全设置innerHTML,避免XSS
element.textContent = content;
} else {
element.innerHTML = '';
element.appendChild(content);
}
return true;
} catch (error) {
console.error(`更新元素 #${elementId} 失败:`, error);
return false;
}
}
/**
* 获取表单值(类型安全版)
* @param {string} formId - 表单ID
* @returns {Record<string, string> | null} 表单数据或null
*/
function getFormDataSafe(formId) {
const form = getElementSafe(formId, 'form');
if (!form) return null;
/** @type {Record<string, string>} */
const data = {};
const formData = new FormData(form);
for (const [key, value] of formData.entries()) {
data[key] = typeof value === 'string' ? value : '';
}
return data;
}
// 使用示例
const button = getElementSafe('submit-btn', 'button');
if (button) {
// button 是 HTMLButtonElement 类型,有完整的方法提示
button.addEventListener('click', handleClick);
}
const success = updateElementSafe('message', '操作成功');
if (success) {
console.log('内容更新成功');
}
const formData = getFormDataSafe('user-form');
if (formData) {
console.log('表单数据:', formData);
// formData.username 是 string 类型
}
核心价值:避免常见的DOM操作错误,如null引用和类型转换错误。
技巧8:第三方库的完美类型集成
场景:在JavaScript项目中使用TypeScript编写的第三方库
// 安装类型定义
// npm install --save-dev @types/lodash
// ✅ 完整的第三方库类型支持
/**
* 深度合并配置对象
* @param {Object} defaultConfig - 默认配置
* @param {Object} userConfig - 用户配置
* @returns {Object} 合并后的配置
*/
function mergeConfig(defaultConfig, userConfig) {
// 使用lodash的merge函数,有完整类型提示
/** @type {import('lodash').MergeWithCustomizer} */
const customizer = (objValue, srcValue) => {
if (Array.isArray(objValue)) {
return objValue.concat(srcValue);
}
};
// lodash的merge函数有完整的类型提示
return _.mergeWith({}, defaultConfig, userConfig, customizer);
}
/**
* 防抖函数包装器
* @template T
* @param {T} func - 要防抖的函数
* @param {number} wait - 等待时间(毫秒)
* @returns {T & { cancel: () => void }} 防抖后的函数
*/
function debounceWithTypes(func, wait) {
// lodash的debounce函数有完整类型提示
const debounced = _.debounce(func, wait);
return /** @type {T & { cancel: () => void }} */ (debounced);
}
/**
* 使用Axios进行API调用
* @param {string} endpoint - API端点
* @param {Object} data - 请求数据
* @returns {Promise<any>} 响应数据
*/
async function apiCall(endpoint, data) {
// 导入Axios实例类型
/** @type {import('axios').AxiosInstance} */
const apiClient = axios.create({
baseURL: '/api',
timeout: 5000
});
try {
const response = await apiClient.post(endpoint, data);
return response.data;
} catch (error) {
// error 是 AxiosError 类型,有完整属性提示
if (error.response) {
console.error('API错误响应:', error.response.status, error.response.data);
} else if (error.request) {
console.error('无响应:', error.request);
} else {
console.error('请求错误:', error.message);
}
throw error;
}
}
// 使用示例
const searchHandler = debounceWithTypes((query) => {
console.log('搜索:', query);
}, 300);
// searchHandler 有完整的类型提示
searchHandler('关键词');
searchHandler.cancel(); // 来自lodash的cancel方法
核心价值:即使在不使用TypeScript的项目中,也能享受类型化第三方库的完整支持。
技巧9:枚举和常量组的类型安全
场景:避免魔法字符串和数字,使用类型安全的常量
// ❌ 魔法字符串和数字
function getStatusText(status) {
if (status === 1) return '待处理';
if (status === 2) return '处理中';
if (status === 3) return '已完成';
return '未知状态';
}
// ✅ 类型安全的枚举和常量
/**
* 订单状态枚举
* @enum {number}
*/
const OrderStatus = {
PENDING: 1,
PROCESSING: 2,
COMPLETED: 3,
CANCELLED: 4
};
/**
* 状态显示文本映射
* @type {Record<OrderStatus[keyof OrderStatus], string>}
*/
const StatusText = {
[OrderStatus.PENDING]: '待处理',
[OrderStatus.PROCESSING]: '处理中',
[OrderStatus.COMPLETED]: '已完成',
[OrderStatus.CANCELLED]: '已取消'
};
/**
* 根据状态码获取状态文本
* @param {OrderStatus[keyof OrderStatus]} status - 状态码
* @returns {string} 状态文本
*/
function getStatusText(status) {
return StatusText[status] || '未知状态';
}
/**
* 权限常量组
* @readonly
* @enum {string}
*/
const Permissions = {
READ: 'read',
WRITE: 'write',
DELETE: 'delete',
ADMIN: 'admin'
};
/**
* 检查用户权限
* @param {string[]} userPermissions - 用户拥有的权限
* @param {Permissions[keyof Permissions]} requiredPermission - 需要的权限
* @returns {boolean} 是否拥有权限
*/
function hasPermission(userPermissions, requiredPermission) {
return userPermissions.includes(requiredPermission);
}
// 使用示例
const currentStatus = OrderStatus.PROCESSING;
console.log(getStatusText(currentStatus)); // '处理中'
// 错误的用法会被提示
// console.log(getStatusText(99)); // ❌ Argument of type '99' is not assignable to parameter...
const userPerms = [Permissions.READ, Permissions.WRITE];
const canDelete = hasPermission(userPerms, Permissions.DELETE); // false
const canWrite = hasPermission(userPerms, Permissions.WRITE); // true
核心价值:消除魔法值,提高代码可读性和可维护性。
技巧10:复杂的业务逻辑类型建模
场景:为复杂业务领域建立完整的类型模型
// ✅ 电商订单系统的完整类型建模
/**
* 商品SKU
* @typedef {Object} ProductSku
* @property {string} id - SKU ID
* @property {string} name - 商品名称
* @property {number} price - 价格(分)
* @property {number} stock - 库存
* @property {Record<string, string>} attributes - 属性(如颜色、尺寸)
*/
/**
* 购物车商品项
* @typedef {Object} CartItem
* @property {ProductSku} sku - 商品SKU
* @property {number} quantity - 数量
* @property {number} [selectedPrice] - 选中时的价格(用于促销)
*/
/**
* 促销规则
* @typedef {Object} PromotionRule
* @property {string} id - 规则ID
* @property {string} name - 规则名称
* @property {'discount'|'gift'|'coupon'} type - 规则类型
* @property {number} [discountRate] - 折扣率(0-1)
* @property {number} [discountAmount] - 折扣金额(分)
* @property {ProductSku} [giftProduct] - 赠品
* @property {Function} condition - 应用条件
* @property {Function} apply - 应用规则
*/
/**
* 订单计算上下文
* @typedef {Object} OrderCalculationContext
* @property {CartItem[]} items - 购物车商品
* @property {PromotionRule[]} promotions - 可用促销
* @property {number} shippingFee - 运费(分)
* @property {number} [pointsUsed] - 使用的积分
*/
/**
* 订单计算结果
* @typedef {Object} OrderCalculationResult
* @property {number} totalAmount - 商品总金额(分)
* @property {number} discountAmount - 折扣金额(分)
* @property {number} shippingFee - 运费(分)
* @property {number} finalAmount - 最终支付金额(分)
* @property {Array<{rule: PromotionRule, discount: number}>} appliedPromotions - 应用的促销
*/
/**
* 计算订单金额
* @param {OrderCalculationContext} context - 计算上下文
* @returns {OrderCalculationResult} 计算结果
*/
function calculateOrder(context) {
const { items, promotions = [], shippingFee = 0, pointsUsed = 0 } = context;
// 计算商品总金额
const totalAmount = items.reduce((sum, item) => {
const price = item.selectedPrice || item.sku.price;
return sum + price * item.quantity;
}, 0);
// 应用促销规则
let discountAmount = 0;
/** @type {Array<{rule: PromotionRule, discount: number}>} */
const appliedPromotions = [];
for (const promotion of promotions) {
if (promotion.condition(items)) {
const discount = promotion.apply(items, totalAmount);
discountAmount += discount;
appliedPromotions.push({ rule: promotion, discount });
}
}
// 计算最终金额
const pointsDiscount = pointsUsed * 100; // 假设1积分=1分钱
const finalAmount = Math.max(0, totalAmount - discountAmount - pointsDiscount + shippingFee);
return {
totalAmount,
discountAmount,
shippingFee,
finalAmount,
appliedPromotions
};
}
// 使用示例
/** @type {ProductSku} */
const productSku = {
id: 'sku-001',
name: '无线耳机',
price: 29900, // 299元
stock: 100,
attributes: { color: '白色', version: '标准版' }
};
/** @type {CartItem} */
const cartItem = {
sku: productSku,
quantity: 2
};
/** @type {PromotionRule} */
const discountRule = {
id: 'promo-001',
name: '满500减50',
type: 'discount',
condition: (items) => {
const total = items.reduce((sum, item) => sum + item.sku.price * item.quantity, 0);
return total >= 50000; // 满500元
},
apply: (items) => 5000 // 减50元
};
/** @type {OrderCalculationContext} */
const context = {
items: [cartItem],
promotions: [discountRule],
shippingFee: 1000, // 10元运费
pointsUsed: 100 // 使用100积分
};
const result = calculateOrder(context);
console.log(`商品总额: ${(result.totalAmount / 100).toFixed(2)}元`);
console.log(`折扣: ${(result.discountAmount / 100).toFixed(2)}元`);
console.log(`运费: ${(result.shippingFee / 100).toFixed(2)}元`);
console.log(`最终支付: ${(result.finalAmount / 100).toFixed(2)}元`);
核心价值:为复杂业务建立清晰的类型模型,使代码结构更清晰,减少业务逻辑错误。
实战总结:JSDoc的最佳实践原则
- 渐进式采用原则
· 从新代码开始:不必立即改造所有旧代码 · 从公共API开始:优先为模块导出的函数和类添加注释 · 从简单到复杂:先掌握@param和@returns,再学习高级特性
- 一致性原则
· 团队统一规范:制定并遵守JSDoc编写规范 · 命名一致:类型命名要有意义且一致(如UserData、ApiResponse) · 格式一致:保持注释格式统一,提高可读性
- 实用性原则
· 不为注释而注释:重点是提升代码质量,不是形式主义 · 关注核心契约:优先明确函数输入输出,再完善细节 · 保持注释更新:代码变更时同步更新注释
- 工具化原则
· 善用IDE支持:利用智能提示和错误检查 · 配置自动化检查:使用ESLint确保注释质量 · 生成文档:使用工具从JSDoc生成API文档
你的JSDoc实践路线图
阶段 目标 关键实践 第1周 基础入门 为所有新函数添加@param和@returns 第1个月 熟练应用 使用@typedef定义复杂类型,配置ESLint检查 第3个月 进阶掌握 使用泛型、函数重载等高级特性 第6个月 领域建模 为业务领域建立完整的类型模型 长期 团队影响 推动团队类型思维,提升整体代码质量
立即开始:本周实战挑战
挑战1:选择一个你最近写的复杂函数,用JSDoc为其添加完整类型注释。
挑战2:为你负责的模块创建公共类型定义文件(types.js)。
挑战3:配置团队的ESLint,添加JSDoc检查规则。
记住:类型思维的价值不在于你写了多少注释,而在于你通过注释理清了多少业务逻辑。
系列回顾:
· 第一篇:学了TypeScript却用不起来?用JSDoc在JavaScript中立即学以致用 · 第二篇:如何让团队平滑拥抱类型思维,避免TypeScript迁移阵痛 · 第三篇:本文
你已经掌握了JSDoc的核心技巧,接下来就是在实际项目中持续实践。真正的成长,发生在你把知识应用到代码中的那一刻。
你在使用JSDoc过程中遇到过哪些有趣的挑战或收获?欢迎在评论区分享你的实战经验!
如果这个系列对你有帮助,请点赞收藏,让更多开发者看到类型思维的魅力。