前言
当 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,归功于
@template { Array<keyof T> } K 元组类型
这段代码将泛型K
的类型约束成(name | gender)[]
,使元组中的每个位置都是泛型T
对象类型上的属性。 - 效果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 已经有了兴趣,可以去详细看看官方文档