原文链接:JSDoc: A Solid Alternative To TypeScript,2023 年 11 月 8 日,by Nwalozie Elijah。翻译时有删改。
许多开发人员喜欢使用 TypeScript,因为它提供类型检查功能。然而,使用 TypeScript 需要额外的转译步骤,这可能很麻烦并且浪费时间。本文将向你展示它的一个替代方案 JSDoc。使用 JSDoc 能同样获得类似 TypeScript 的类型提示能力,但同时你编写的只是纯 JavaScript 代码,这样就能获得最快的开发时间和更好的文档!
JavaScript 一直处于近年来最常用的脚本语言之一的地位。它以在 Web 平台上编写脚本的便捷性而闻名。随着语言本身的发展,它从最开始蹭 Java 热度的“玩具”语言,变成了一种成熟的语言,还能用来构建大的应用了。
不幸的是,随着深入使用,JavaScript 语言本身的缺陷也被保留出来,包括:
- 缺乏静态类型检查。JavaScript 是一门动态语言,有较为宽松的限制。比如:定义的函数参数在调用时不提供也行。静态类型语言(例如 Java)就不是这样了,因为它会在编译时报错,但 JavaScript 默认不提供这方面的支持,就导致某些错误会渗透到 Javascript 应用程序的生产环境中
- 在大项目中很难扩展和维护。JavaScript 没有提供一种强有力的机制来管理大型代码库,这使得随着时间的推移扩展和维护项目变得困难。
TypeScript 出现
2014 年,微软推出了 Typescript v1.0。这改变了整个 JavaScript 生态系统。
TypeScript 是 JavaScript 语言的超集,它解决了上一节提到的问题以及更多其他问题。这使得它越来越受欢迎。
State of Js survey 2022 展示的 TypeScript 使用率在上升。
TypeScript 虽然解决了很多问题,但也有缺点。
本文我们将研究 TypeScript 的一个非常好的替代方案——JSDoc,它解决了静态类型和可扩展问题,同时还消除了 JavaScript 生态系统中 TypeScript 的缺点。
JSDoc 是什么?
JSDoc 是基于 JavaScript 语言注释功能建立起的一套文档系统。可以帮助你在编写 JavaScript 代码的同时,通过使用包含 JSDoc 语法的注释获得文档支持。
JSDoc 语法有多种用途,包括为变量声明类型、指定函数参数和返回值的类型、记录和提供函数的使用方式、避免拼写错误等。这些特性与 TypeScript 类似,可以被像 VS Code 这类现代代码编辑器利用,为程序员提供构建、使用或维护代码的支持。
JSDoc vs Typescript
JSDoc 和 TypeScript 都解决了编写和维护纯 JavaScript 代码的问题。然而,他们使用了不同的策略,各有优缺点。
JSDoc 相对于 Typescript 的优点:
- 灵活并且代码兼容:JSDoc 只是被定义的一种特殊的 JavaScript 注释,这意味着它可以添加到任何 JavaScript 代码库中(无论语言版本如何),并且它不像 TypeScript 那样与编译器绑定
- 提供代码注释支持:JSDoc 不仅仅可以用于类型检查。它可用于添加文档说明、描述函数如何工作,并且基于此生成文档网站,所有这些都为增强代码的可维护性和理解提供了价值
- 无需编译步骤:这是从 TypeScript 切换到 JSDoc 的最直接的原因之一。 TypeScript 需要通过编译器将代码编译成 Javascript,以便浏览器可以理解。而 JSDoc 不需要任何编译步骤,因为它本质上只是“注释”,这是 Javascript 本身支持的功能。与每次进行更改时使用必要的 Typescript 构建流程相比,这可以简化并提高开发效率
使用 JSDoc 的缺点
虽然 JSDoc 比 TypeScript 有很多优势。但现状是 Typescript 使用率不断攀升,被大家越来越多地采用,这是有原因的。以下是 Typescript 相对于 JSDoc 的一些优点:
- 更强的静态类型支持:TypeScript 为类型提供了强大的模型,并能在编译时捕获这些错误。但 JSDoc 就不支持,这些错误就直接留在当时的代码中,也没有手段去要求强制执行修正
- 类型推断支持:TypeScript 可以从值推断类型。这有助于减少显式类型注释并让代码库更加简洁
- 转译支持:TypeScript 可以通过其 polyfill 功能使用 JavaScript 语言的最新功能(甚至是更加早期的提案功能),有效地将这些最新代码转换成在低版本浏览器中也能运行的版本
如何使用 JSDoc:基础知识
JSDoc 存在很久了,因此所有现代编辑器中都广泛支持它,开箱即用,无需任何安装。
在 .js 文件中添加 JSDoc 至此,就是增加注释,是通过添加带有额外星号(*)的注释来完成的。
// Normal Javascript Comment 1
/* Normal Javascript Comment 2 */
/**
JSDoc 需要使用 2 个星号
*/
接下来,我们介绍一些基本功能。
添加代码描述
/** JSDoc 用来服务的语言 */
const language = "JavaScript"
为变量添加类型
/**
* 本篇文章的作者
* @type {string}
*/
const writerName = "Elijah"
以上注解表示变量 writerName 是字符串类型。
为对象和数组添加类型
/**
* @type {Array<string>}
*/
const colours = ['red', 'blue', 'green']
/**
* @type {Array<number[]>}
*/
const primeNumbers = [1, 2, 3, 5, 7]
以上 2 种方法都是有效的 JSDoc 注解(与 TypeScript 一样)。
而对象类型则可以通过 @typedef 指令来创建。
/**
* @typeof {Object} User - A user schema
* @property {number} id
* @property {string} username
* @property {string} email
* @property {Array<number>} postLikes
* @property {string[]} friends
*/
/** @type {User} */
const person1 = {
id: 847,
username: "Elijah",
email: "elijah@user.com",
postLikes: [44, 22, 24, 39],
friends: ['fede', 'Elijah']
}
/** @type {User} */
const person2 = {
id: 424,
username: "Winston",
email: "winston@user.com",
postLike: [18, 53, 98],
friends: ['Favour', 'Jane']
}
为函数添加类型(参数、返回值和预期错误类型)
/**
* Divide two numbers.
* @param {number} dividend - The number to be divided.
* @param {number} divisor - The number to divide by.
* @returns {number} The result of the division.
*/
function divideNumbers(dividend, divisor) {
return dividend/divisor;
}
@param关键字后面跟参数类型定义,还可以使用连字符 - 添加参数描述。
@returns 关键字用于定义函数返回类型。这对于大型函数特别有用,因为这类函数一般很难观察它预期的返回类型。
此外,你可以使用 @throws 指令添加函数可能的抛错类型。
接下来,改进 divideNumbers 函数,增加除数为零时的抛错支持。
/**
* Divide two numbers.
* @param {number} dividend - The number to be divided.
* @param {number} divisor - The number to divide by.
* @returns {number} The result of the division.
* @throws {ZeroDivisionError} Argument divisor must be non-zero
*/
function divideNumbers(dividend, divisor) {
if (divisor === 0) {
throw new DivisionByZeroError('Cannot Divide by zero')
}
return dividend/divisor;
}
你可以在 @throws 中同时指定错误类型以及错误描述。
/**
* Custom error for division by zero.
*/
class DivisionByZeroError extends Error {
constructor(message = "Cannot Divide By Zero") {
super(message);
this.name = "DivisionByZeroError";
}
}
由于 JavaScript 本身并不强制你处理错误,因此这样做一定程度上有助于改善代码协作、便于维护。
为 class 添加类型(描述、构造函数以及方法)
更进一步,你还可以使用 JSDoc 为 class 提供类型支持。
/**
* A Rectangle Class
* @class
* @classdec A four-sided polygon with opposite sides of equal length and four right angles
*/
class Rectangle {
/**
* Initializing a Rectangle object.
* @param {number} length - The length of the rectangle.
* @param {number} width - The width of the rectangle.
*/
constructor(length, width) {
this.length = length;
this.width = width;
}
/**
* Calculate the area of the Rectangle
* @returns {number} The area of the rectangle.
*/
calculateArea() {
return this.length * this.width;
}
/**
* Calculate the perimeter of the rectangle.
* @returns {number} The perimeter of the rectangle.
*/
calculatePerimeter() {
return 2 * (this.length + this.width);
}
}
上面是一个简单的矩形类,提供了 2 种方法分别用来计算其面积和周长。
@class 关键字用于表示这个函数需要使用 new 关键字调用,@classdec 用于类的描述。为类添加类型时,重要的是进一步添加类型和描述。
- 构造函数
- 所有属性和方法
我们使用 @params 关键字来提供需要传递到构造函数中的参数的类型和描述。类中的方法的类型化方式与函数相同,这在上一节中已介绍过,就不再赘述。
改进通用代码文档
除了向代码添加基本类型之外,JSDoc 还有很多方法可以帮助提高可读性性。这里有几个:
- 添加代码作者:可以使用
@author指令添加作者姓名和电子邮件
/**
* Possible title for this article
* @type {string}
* @author Elijah [elijah@example.com]
*/
const articleTitle = "Demystifying JSDoc"
- 用法示例:你还可以添加代码片段,展示如何使用,这对于复杂的代码块特别有用
/**
* Sums of the square of two numbers a**2 + b**2
* @example <caption>How to use the sumSquares function</caption>
* // returns 13
* sumSquares(2, 3)
* @example
* // returns 41
* sumSquares(4, 5)
* // Typing the function
* @param {number} a - The first number
* @param {number} b - The second number
* @returns {Number} Returns the sum of the squares
*/
const sumSquares = function(a, b){
return a**2 + b**2
}
我们使用 @example 指令来实现这一点,也可以使用 <caption> 标签作为标题。
- 版本控制:你还可以使用
@version指令指定项目的版本
/**
* @version 1.0.0
* @type {number}
*/
const meaningOfLife = 42
- 有用的链接:通常,你可能希望向用户提供一些跳转链接,他们可以获得有关代码的更多知识。它可能是 GitHub 仓库、一篇教程、一篇博客等。为此,需要两个指令来帮助实现:
@link和@tutorial
/**
* How to use the link tags
* Also see the {@link https://jsdoc.app/tags-inline-link.html official docs} for more information
* @tutorial getting-started
*/
function myFunction (){
}
@link 指令将“official docs”渲染成指向某个地址的文字链接。而 @tutorial 指令则用于将用户引导至生成文档上的相关教程链接。
- 创建模块:可以使用文件顶部的
@module指令在 JSDoc 中创建模块,当前文件就成一个模块了。模块被分组在生成的文档网站上的单独部分中。
// jsdoc.js
/** @module firstDoc */
//The rest of the code goes here
转换 JSDoc 文件
使用 JSDoc 的最大优点之一是能够将 JSDoc 文件转换为生成文档网站——甚至是 Typescript,这样他们就可以获得使用 Typescript 的好处。
从 JSDoc 文件生成文档网站
如上所述,你可以按照以下步骤生成更具可读性的 GUI:
- 安装 jsdoc
$ npm install -g jsdoc
- 对目标文件运行 jsdoc
$ jsdoc path/to/file.js
- 打开生成的网站。
jsdocCLI 会将文档自定输出到 out 文件夹,然后在浏览器中打开out/index.html
这是默认 jsdoc 生成的模板的样子,但你可以设置成不同的模板配置。
从 JSDoc 生成 .d.ts 文件
TypeScript 中的 .d.ts 文件表示声明文件,你可以使用以下步骤从 JSDoc 代码生成这些文件:
- 在项目文件夹中安装 tsd-jsdoc
$ npm install tsd-jsdoc
- 生成
.d.ts文件
对于单个文件。
$jsdoc -t node_modules/tsd-jsdoc/dist -r our/jsdoc/file/path.js
对于多个文件。
$jsdoc -t node_modules/tsd-jsdoc/dist -r file1.js file2.js file3.js ...
对于整个文件夹。
$jsdoc -t node_modules/tsd-jsdoc/dist -r src
它会将文件中的所有类型合并到 单个文件 out/types.d.ts 中。
注意:这假设你已经安装了上一节中的 jsdoc。如果没有,请在运行此步骤之前先安装它。
结论
至此,我们已经学习了使用 JSDoc 以及从 JSDoc 代码生成类型和文档网站的基础知识。当 Typescript 编译/构建步骤对生产力产生负面影响时,JSDoc 特别有用。对遗留代码库来说 JSDoc 也很有用。
Rich Harris(Svelte 和 SvelteKit 的创建者)也将整个 Svelte 和 SvelteKit 仓库从 TypeScript 改用 JSDoc。另外,TypeScript 也添加了对许多 JSDoc 声明的支持(来源)。