JSDoc的进阶使用

前言


当 TypeScript 越来越流行时,许多项目选择使用 TypeScript 进行开发,因为它带来了许多好处,这一点大家都有所耳闻。

然而,一些旧项目仍然采用传统的 JavaScript 技术,因为进行全面重构成本太高。

在这种情况下,我们就需要介绍 JSDoc(也称为文档注释)作为一种解决方案。

JSDoc 是一种在 JavaScript 中使用注释来提供类型信息和文档说明的方法。 JSDoc 不仅可以提供类型提示,还可以生成文档,帮助开发人员更好地理解代码。

JSDoc 的出现为传统 JavaScript 项目引入了一种新的方式,使得即使在没有 TypeScript 的情况下,也能够获得一些类似 TypeScript 的好处。

重新看待JSDoc


相信大家都曾见过下面这样的写法,通过使用 @param 标签标记函数参数 的名称、类型和描述,使用 @returns 标签标记函数返回值

javascript 复制代码
/**
 * 对象合并,如果存在相同的属性,优先使用 target 的属性.
 */
function merge(source, target) { ... }

/**
 * 对象合并,如果存在相同的属性,优先使用 target 的属性.
 * @param { Object } source 源对象
 * @param { Object } target 目标对象
 * @returns { Object } 返回合并后的新对象
 */
function merge(source, target) { ... }

但如果是以下这样呢,是不是就有点TS的味道了(手动狗头)

javascript 复制代码
/**
 * 对象合并,如果存在相同的属性,优先使用 target 的属性
 * @template { Record<string, any> } T
 * @template { Record<string, any> } U
 * @param { T } source 源对象
 * @param { U } target 目标对象
 * @returns { Omit<T, keyof (T | U)> & U } 返回合并后的新对象
 */
function merge(source, target) { ... }

const test = merge({ name: 'test', gender: '男' }, { age: 18, gender: 1 })

可以在 JSDoc 中使用 TS 语法,这样的话,就算在 JS 开发的项目中,"体操哥"不也有发挥空间了嘛。

假设你已经具备了一定的TS水平,那接下来让我们直接进入正题。如果你不太了解甚至为泛型而感到头疼,《TS类型的进阶使用 - 掘金》这篇文章希望能够帮到你。

泛型


当谈到 TS 时,最具挑战性的部分莫过于泛型。泛型可以说是 TS 的精髓,它为我们提供了在编写灵活且类型安全的代码时所需的强大工具。泛型使得我们能够以一种抽象的方式来处理不同类型的数据,从而提高了代码的灵活性和可维护性。

在 JSDoc 中使用 @template 标签声明泛型

javascript 复制代码
/**
 * @template T 声明泛型
 * @param { T } value 入参
 * @returns { T } 返回值
 */
function identity(value) { } // function identity<T>(value: T): T

const a = identity("text"); // string
const b = identity(123); // number

可以从提示中看到 identity 已经变成了一个标准的泛型函数了,传入什么类型返回什么类型

使用逗号或多个标签声明多个泛型参数:

javascript 复制代码
/**
 * @template T,U,V
 * @template W,X
 */

还可以在泛型名称之前进行泛型约束

javascript 复制代码
/**
 * @template { unknown[] } T
 * @param { T } value 要获取长度的数组
 * @returns { T['length'] } 数组的长度
 */

可以看到这些全是数组的方法和属性,编辑器已经能够识别出你的 T 是一个数组了

最后,还可以为泛型设置默认类型

javascript 复制代码
/**
 * @template [T = string] 声明泛型并设置默认类型为string
 * @param { T } value 入参
 * @returns { T } 返回值
 */
function identity(value = '') { }

omit函数

结合之前所讲的泛型,来看看一个经典的函数------omit

javascript 复制代码
/**
 * omit函数 去除对象中的值得属性
 * @template { Record<string, any> } T 对象类型
 * @template { Array<keyof T> } K 元组类型
 * @param { T } target 目标元素
 * @param { K } keys 属性
 * @returns { Omit<T, K[number]> } 新对象
 */
function omit(target, keys) { ... }

效果:

总结:

  1. 能得到效果1,归功于 @template { Array<keyof T> } K 元组类型 这段代码将泛型 K 的类型约束成 (name | gender)[] ,使元组中的每个位置都是泛型 T 对象类型上的属性。
  2. 效果2则归功于多个泛型参数以及泛型约束,K[number] 将元组转成联合类型,假设函数的形参keys[name, gender]那么将转换为name | gender,所以图中的转换为 [name]=> name;然后再结合 Omit 即可达到我们想要的效果了。能够拿到 keys 具体的类型十分关键,想想如果只有一个泛型参数可以做到吗?

复杂类型


在前面,我们所讨论的类型中,都是一些简单的类型,一行代码就可以搞定的,如果某个函数的类型特别复杂,需要写很多代码,这时候又该如何应对呢?

类型导入

使用 import 从其他文件中导入类型

javascript 复制代码
// 文件:utils.d.ts

// 说明:过滤掉对象中属性值为指定内容的属性,使用 as 关键字来进行过滤
export type RemovePropertiesByValue<T extends Record<string, any>, V extends readonly unknown[]> = {
    [K in keyof T as T[K] extends V[number] ? never : K]: T[K]
}
javascript 复制代码
// 文件:JSDoc.js

/**
 * 去除对象中属性值为空的属性
 * @template { Record<string, any> } T
 * @template { readonly any[] } [V = [undefined, null]]
 * @param { T } target 要处理的对象
 * @param { V } values 自定义过滤的value值,默认为[undefined, null]
 * @returns { import("./utils").RemovePropertiesByValue<T, V> } 处理后的对象
 *            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 */
function filterObjectEmpty(target, values = [undefined, null]) { }
const test4 = filterObjectEmpty({ name: undefined, age: 18, gender: '', address: null }) // 测试代码

我们从 utils.d.ts 类型文件中使用import导入了 RemovePropertiesByValue 类型,此类型接收两个泛型参数。
@returns { import("./utils").RemovePropertiesByValue<T, V> } 导入了此类型并将 泛型T 和 泛型V 作为参数传递给 RemovePropertiesByValue

既然如此,还有什么类型是无法用 JSDoc 处理的呢,我想只有我们无法写的类型吧 本文的重点(泛型)已经讲完,如果你对 JSDoc 已经有了兴趣,可以去详细看看官方文档

参考资料


  1. Documentation - JSDoc Reference
  2. 块级标签 | JSDoc中文文档 | JSDoc中文网
相关推荐
袁煦丞15 分钟前
【局域网秒传神器】LocalSend:cpolar内网穿透实验室第418个成功挑战
前端·程序员·远程工作
江城开朗的豌豆16 分钟前
Vuex数据突然消失?六招教你轻松找回来!
前端·javascript·vue.js
好奇心笔记27 分钟前
ai写代码随机拉大的,所以我准备给AI出一个设计规范
前端·javascript
江城开朗的豌豆27 分钟前
Vue状态管理进阶:数据到底是怎么"跑"的?
前端·javascript·vue.js
用户214118326360228 分钟前
dify案例分享-Dify v1.6.0 重磅升级:双向 MCP 协议引爆 AI 生态互联革命
前端
程序员海军28 分钟前
AI领域又新增协议: AG-UI
前端·openai·agent
我想说一句31 分钟前
React待办事项开发记:Hook魔法与组件间的悄悄话
前端·javascript·前端框架
真夜31 分钟前
CommonJS与ESM
前端·javascript
LaoZhangAI31 分钟前
GPT-image-1 API如何传多图:开发者完全指南
前端·后端
G等你下课33 分钟前
从点击到执行:如何优雅地控制高频事件触发频率
前端·javascript·面试