使用 JSDoc 类型增强你的 JavaScript
有很多原因导致您无法或不想在项目中使用 TypeScript。一个常见的原因是您使用的旧代码库与 TypeScript 不兼容。或者,切换到 TypeScript 比任何人都说的要难。无论出于何种原因,您都只能使用 JavaScript。但这并不意味着您必须完全放弃 TypeScript 的优势。在本文中,我们将探索 JSDoc 类型的魔力,有了它,您可以立即使用大多数 TypeScript 功能。🧙♂️
那么,让我们开始吧!🏊♂️
这是我的第一篇博文,非常感谢大家的反馈。如果您有任何问题或建议,欢迎在下方留言。
以下是我将在本文中讨论的主题的概述:
-
TypeScript 类型:在这里,我们将了解如何在 JSDoc 中使用 TypeScript 类型。如果您不熟悉 TypeScript,也不用担心。我会详细解释您需要了解的一切。
-
更多 JSDoc 优点:在介绍了如何向您的项目添加类型之后,我们将了解 JSDoc 提供的一些其他功能。
-
JSDoc 实践:现在您已经了解了 JSDoc 的强大功能,并希望在项目中使用它。但是该如何开始呢?在本节中,我们将了解如何设置 VSCode,以获得最佳的 JavaScript 键入体验。
-
最佳实践:最后,我们将了解在项目中使用 JSDoc 的一些最佳实践。
TypeScript 类型
字符串、数字、布尔值等🎭
在 TypeScript 中,最常见的类型是原始类型。这些类型很特殊,因为它们代表了语言的最低层构建块。使用小写字母书写原始类型非常重要,因为这有助于避免与类或接口混淆。例如,如果您使用String
而不是string
,它可能会误认为是全局构造函数,从而导致潜在的混淆和错误。您可以在现代 JavaScript 教程String
中阅读更多相关信息。
以下是在 TypeScript 和 JSDoc 中使用原始类型的方法:
// TypeScript
const name: string = 'John Doe';
const age: number = 25;
const average: number = 3.14;
const isActive: boolean = true;
const nullable: number | null = null;
const unassigned: string | undefined;
// JavaScript JSDoc
/** @type {string} */
const name = 'John Doe';
/** @type {number} */
const age = 25;
/** @type {number} */
const average = 3.14;
/** @type {boolean} */
const isActive = true;
/** @type {number | null} */
let nullable = null;
nullable = 5;
/** @type {string | undefined} */
let unassigned;
unassigned = 'John Doe';
请注意,JSDoc 注释以两个星号开头/**
,以一个常规星号加一个正斜杠结尾*/
。如果注释块以单个星号开头,则会被视为常规注释,JSDoc 不会对其进行解析。要添加 JSDoc 注释,只需将注释块直接放在要记录的代码元素之前即可。
数组和元组🍱
TypeScript 中的数组和元组可帮助您处理项目列表。在 JSDoc 中,有两种方法可以定义它们的类型。第一种是使用[]
语法,这是最常见且被广泛接受的。第二种是使用Array
泛型类型,这种类型不太常见。
// arrays
const numbers: number[] = [1, 2, 3];
const names: Array<string> = ['John', 'Jane', 'Doe'];
虽然[]
语法更简单易读,但当有多维数组或复杂类型时,它会变得更难阅读。在这种情况下,Array
泛型更具可读性。最终,这取决于个人喜好,你可以选择你喜欢的。
// More readable as Array<Array<number>> since it
// clearly shows the nesting structure which makes
// it easier to visualize the array in our heads
const matrix: Array<Array<number>> = [[1, 2], [3, 4]];
const matrix: number[][] = [[1, 2], [3, 4]];
// Using JSDoc
/** @type {number[][]} */
const numbers = [[1, 2], [3, 4]];
/** @type {Array<Array<number>>} */
const numbers = [[1, 2], [3, 4]];
元组类似于数组,但它们的长度是固定的,并且每个元素都有特定的类型。当你想用固定数量的元素来表示一个值时,元组非常有用,因为每个元素都有特定的类型。例如,你可以使用元组来表示二维平面中的坐标,其中第一个元素是 x 坐标,第二个元素是 y 坐标:
// tuples
const coordinates: [number, number] = [40.7128, -74.0060];
const person: [string, number] = ['John Doe', 30];
// Using JSDoc
/** @type {[number, number]} */
const coordinates = [40.7128, -74.0060];
/** @type {[string, number]} */
const person = ['John Doe', 30];
对象和接口🏢
TypeScript 允许您使用对象类型和接口来定义对象的结构。{ property: Type }
当结构简单且不太可能在整个代码库中重复使用时,请使用内联对象类型语法 ( ) 来定义对象类型。如果您的类型很复杂,或者预计同一结构会在整个代码库中多次重复使用,那么维护类型就会变得越来越困难,从而更容易引入错误。在这种情况下,最好使用interface
关键字来定义可重用的对象类型。当您想为特定函数或组件创建临时类型,而又不想用单独的接口声明弄乱代码时,内联对象类型更合适。
// inline object typing
const user: { name: string; age: number } = {
name: 'John Doe',
age: 25,
};
// interface typing
interface User {
name: string;
age: number;
}
const user: User = { name: 'John Doe', age: 25 };
// Using JSDoc
/** @type {{ name: string; age: number }} */
const user = { name: 'John Doe', age: 25 };
/** @type {User} */
const user = { name: 'John Doe', age: 25 };
我们可以使用标签在 JSDoc 中定义接口和自定义类型@typedef
。标签后面跟着类型以及我们想要赋予它的名称。定义类型有两种方法:第一种是使用@property
标签定义类型的每个属性。这允许你为每个属性添加一个描述,以揭示更多关于该属性、其用途以及如何使用的信息。第二种是使用@typedef
标签以内联方式定义类型。第二种方法更简洁易读,但它不允许你为每个属性添加描述。
// Using @property tag
/**
* @typedef {Object} User
* @property {string} name The user's full name.
* @property {number} age The user's age in days. We use days
* instead of years to avoid dealing with leap years.
*/
/** @type {User} */
const user = { name: 'John Doe', age: 25 };
// Using inline type definition
/** @typedef {{ name: string; age: number }} User */
const user = { name: 'John Doe', age: 25 };
可选属性📝
要将属性标记为可选,请在属性名称后添加一个问号 ( ?
)。这会告诉 TypeScript 该属性可能存在于对象中,也可能不存在于对象中。您可以使用该标签将属性名称括在方括号 ( )@property
中,在 JSDoc 中将属性标记为可选。[property]
// Using optional properties
interface User {
name: string;
age?: number;
}
// Using @property tag
/**
* @typedef {Object} User
* @property {string} name The user's full name.
* @property {number} [age] The user's age.
*/
枚举和联合🎲
TypeScript 引入了枚举和联合,分别用于管理一组命名常量和组合多种类型。JavaScript 没有枚举,但我们可以使用@enum
标签告诉 JSDoc 将常规对象视为枚举。标签@typedef
可用于定义联合类型。您也可以使用类型Record<string, string>
来定义枚举,但@enum
标签更简洁易读。稍后将详细介绍实用程序类型。
// enums
/** @enum {string} */
const Color = {
Red: 'red',
Green: 'green',
Blue: 'blue',
Age: 42, // Error: Type 'number' is not assignable to type 'string'
};
/** @type {Color} */
const color = Color.Red;
// unions
/** @typedef {string | number} StringOrNumber */
/** @type {StringOrNumber} */
let value = 'Hello'; // Can be a string
value = 42; // Or a number
类型别名🏷️
类型别名是为现有类型创建新名称的一种方式。通过为复杂类型赋予更有意义的名称,它们可以提高代码的可读性和可维护性。在 TypeScript 中,有一个type
用于创建类型别名的关键字。然而,在 JSDoc 中,你可以使用@typedef
我们之前见过的标签来定义类型别名。
// In TypeScript
type Age = number;
type Name = string;
type User = { name: Name; age: Age };
const user: User = { name: 'John Doe', age: 25 };
// Using JSDoc
/** @typedef {number} Age */
/** @typedef {string} Name */
/** @typedef {{ name: Name; age: Age }} User */
/** @type {User} */
const user = { name: 'John Doe', age: 25 };
文字类型🔠
TypeScript 中的字面量类型是一种定义只能为特定值的类型的方法。它们可以用于字符串、数字或布尔值。要创建字面量类型,只需将所需的值用作类型即可。
// In TypeScript
type Red = 'red';
type Blue = 'blue';
type Green = 'green';
type Color = Red | Blue | Green;
const color: Color = 'red'; // Allowed
color = 'yellow'; // Error: Type '"yellow"' is not assignable to type 'Color'
// In JSDoc
/** @typedef {'red' | 'blue' | 'green'} Color */
/** @type {Color} */
const color3 = 'red'; // Allowed
color3 = 'yellow'; // Error: Type '"yellow"' is not assignable to type 'Color'
实用类型🧰
TypeScript 提供了一组预定义的实用类型,可帮助您操作和转换类型。这样,您就可以基于现有类型创建新类型。其中最常见的类型包括Partial
、Readonly
、和。此外,还有更多可用的Record
类型,您可以在TypeScript 文档中找到列表。Pick
Omit
interface User {
name: string;
age: number;
}
// Partial: Make all properties in User optional
type PartialUser = Partial<User>;
// {
// name?: string | undefined;
// age?: number | undefined;
// }
// Readonly: Make all properties in User readonly
type ReadonlyUser = Readonly<User>;
// {
// readonly name: string;
// readonly age: number;
// }
// Record: Create a new type with keys from a union and values of a specific type
type UserRole = 'admin' | 'user';
type Roles = Record<UserRole, boolean>;
// {
// admin: boolean;
// user: boolean;
// }
// Pick: Create a new type by picking specific properties from another type
type UserWithoutAge = Pick<User, 'name'>;
// {
// name: string;
// }
// Omit: Create a new type by omitting specific properties from another type
type UserWithoutName = Omit<User, 'name'>;
// {
// age: number;
// }
这些实用程序类型可以在 JSDoc 中像这样使用:
/** @typedef {{ name: string; age: number }} User */
/** @typedef {Partial<User>} PartialUser */
/** @typedef {Readonly<User>} ReadonlyUser */
/** @typedef {Record<'admin' | 'user', boolean>} Roles */
/** @typedef {Pick<User, 'name'>} UserWithoutAge */
/** @typedef {Omit<User, 'name'>} UserWithoutName */
泛型🧬
泛型是一种创建可复用组件的方法,这些组件可以处理多种类型。泛型允许您定义一个动态类型,该类型可以在多个位置以不同类型的方式使用。听起来很复杂,但您可以将其视为函数参数,其中要创建的类型是函数,泛型类型是参数。然后,该函数/类型使用泛型类型创建一个新类型。要创建一个新类型,请使用<>
语法并指定其名称。然后,您可以在类型定义中使用该泛型。要指定多个泛型类型,请使用逗号分隔的列表。在以下示例中T
,和U
是泛型类型。
// In TypeScript
type TypeT<T> = T;
type TypeTorU<T, U> = T | U;
type TypeBoolean = TypeT<boolean>;
type TypeStringOrNumber = TypeTorU<string, number>;
const value: TypeStringOrNumber = 'Hello'; // Allowed
const value2: TypeBoolean = true; // Allowed
// In JSDoc
/**
* @template T
* @typedef {T} TypeT
*/
/**
* @template T,U
* @typedef {T | U} TypeTorU
*/
/** @typedef {TypeT<boolean>} TypeBoolean */
/** @typedef {TypeTorU<string, number>} TypeStringOrNumber */
映射类型🗺️
映射类型允许您通过转换现有类型的属性来创建新类型。您可以将其视为map
JavaScript 中的数组方法。当您想要根据一组键修改对象类型的形状,或对类型的属性应用特定转换时,映射类型尤其有用。要创建映射类型,请在类型定义中使用in
和关键字。keyof
关键字in keyof
用于迭代类型的键。P
表示的键T
,是的T[P]
属性类型:P
T
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
interface User {
name: string;
age: number;
}
type NullableUser = Nullable<User>;
// {
// name: string | null;
// age: number | null;
// }
在 JSDoc 中,您可以使用@template
标签来定义泛型,使用@typedef
标签来定义映射类型。
/**
* @template T
* @typedef {{ [P in keyof T]: T[P] | null }} Nullable<T>
*/
/** @typedef {{ name: string; age: number }} User */
/** @typedef {Nullable<User>} NullableUser */
// {
// name: string | null;
// age: number | null;
// }
条件类型🌓
TypeScript 中的条件类型允许您根据条件创建类型,从而实现更灵活、更动态的类型。您可以像if
JavaScript 中的语句一样理解它们。它们在类型定义中使用三元运算符语法。extends
用于定义条件, 和?
用于:
定义条件为真或为假时返回的类型。
type IsString<T> = T extends string ? 'yes' : 'no';
type A = IsString<string>; // 'yes'
type B = IsString<number>; // 'no'
// A and B are now literal types of 'yes' and 'no', respectively
// JSDoc
/**
* @template T
* @typedef {T extends string ? 'yes' : 'no'} IsString<T>
*/
/** @typedef {IsString<string>} A */ // 'yes'
/** @typedef {IsString<number>} B */ // 'no'
索引访问类型🔍
对于最后一个类型特性,我们将探索索引访问类型。索引访问类型允许您访问另一个类型中属性的类型。当您想要提取特定属性的类型或基于现有类型的属性创建更复杂的类型时,它们会非常有用。
interface User {
name: string;
age: number;
}
type UserName = User['name']; // string
type UserAge = User['age']; // number
// JSDoc
/** @typedef {{ name: string; age: number }} User */
/** @typedef {User['name']} UserName */ // string
/** @typedef {User['age']} UserAge */ // number
强制类型🎭
既然我们已经体验过 TypeScript 的魅力,让我们看看如何使用类型转换来告诉编译器你比它更了解。当你想覆盖编译器的类型推断时,它会很有用。要转换类型,请使用@type
标签并指定要转换的类型。注意,你必须将要转换的表达式放在括号中。
const input = document.querySelector('input[type="text"]');
// TypeScript infers the type of input to be `Element | null`
// But now if we try to access a property that is not available
// on `Element`, we get an error
if (input) {
input.value; // ERROR: Property 'value' does not exist on type 'Element'
}
// To fix this we can cast the type to `HTMLInputElement` like this:
if (input) {
const value = /** @type {HTMLInputElement} */ (input).value;
// Now TypeScript knows that the type of `value` is `string`
}
借助这些强大的功能,您可以创建动态且富有表现力的类型。在继续之前,我最后想提一下,您可以安装一些库,例如type-fest或utility-types ,以便为项目添加更多类型。这些库包含许多可以在项目中使用的实用类型。
太棒了!现在我们已经探索了 TypeScript 提供的不同类型特性,让我们看看还能用 JSDoc 做什么。
更多 JSDoc 优点📚
还有一些 JSDoc 标签你应该了解。这些标签与类型没有直接关系,但在使用 JSDoc 时仍然很有用。让我们来看看它们。
快速回顾📝
@type
用于定义变量的类型。@typedef
用于定义类型别名。@property
或@prop
用于定义对象的属性。@template
用于定义泛型。@enum
用于定义枚举。@param
用于定义函数的参数。@returns
或@return
用于定义函数的返回类型。
让我们继续讨论更多标签。
查看和链接标签
和标签可帮助您连接文档的不同部分。当您想要指向相关项目(例如类或类型)时,请使用 和 标签。 和@see
标签用于链接到与您当前文档无直接关联的其他文档。您可以使用这两个标签链接到项目内部的内容或其他在线资源。@link
@see
@link
使用@link
标签,您还可以将读者引导至文档中的特定章节或特定代码行。要链接到某个章节,请使用 # 符号,后跟章节名称。要链接到某行代码,请使用#L
符号并添加要指向的行号。要引用多行代码,请使用-
符号分隔起始行号和结束行号(例如#L6-L13
)。
/** @typedef {{ name: string; age: number }} Person */
/**
* @see {Person}
* @see {@link https://webry.com}
* @link https://github.com/sindresorhus/type-fest#install
* @link https://github.com/sindresorhus/type-fest/blob/main/source/primitive.d.ts#L6-L13
*/
示例标签
该@example
标签用于向文档添加示例。您可以使用它来展示如何使用某个函数,或者展示某种类型的工作原理。您还可以使用它来展示如何使用某个库,或者展示如何使用库中的某个特定功能。
/**
* @param {number} a
* @param {number} b
* @returns {number}
* @example
* add(1, 2) // 3
*/
摘要和描述标签
标签@summary
用于为您的文档添加简短描述。它用于快速概述您正在记录的项目的功能。标签@description
用于为您的文档添加更长的描述。它用于提供有关您正在记录的项目的更详细信息。
/**
* @summary Adds two numbers together.
* @description This function adds two numbers together and returns the result.
* @param {number} a
* @param {number} b
* @returns {number}
*/
JSDoc 注释的格式化🎨
您可以在 JSDoc 注释中使用 Markdown。这意味着您可以使用标题、列表和其他 Markdown 功能来提升文档的可读性。您还可以使用一些 HTML 标签来<br>
为文档添加更多样式。
/**
* @param {number} a
* @param {number} b
* @returns {number}
* @example
* ### Example usage
* You can use this **function** _like_ ~this~:
* ``js
* add(1, 2) // 3
* ``
*/
function add(a, b) {
return a + b;
}
您还可以使用更复杂的 Markdown 功能,例如列表和表格。查看Adam Pritchard 的Markdown 速查表了解更多信息。
其他 JSDoc 标签📚
您可能会发现其他一些有用的 JSDoc 标签:
@function
或@func
:记录函数或方法。@class
:记录类构造函数。@constructor
:表示某个函数是某个类的构造函数。@extends
或@augments
:表示一个类或类型扩展了另一个类或类型。@implements
:表示类或类型实现接口。@namespace
:将相关项目(例如函数、类或类型)归入一个公共命名空间下。@memberof
:指定某项属于某个类、命名空间或模块。@ignore
:告诉 JSDoc 从生成的文档中排除某个项目。@deprecated
:将函数、类或属性标记为已弃用,表示不应再使用。@since
:记录项目引入时的版本。以及更多其他内容。您可以在此处找到完整的 JSDoc 标签列表。
好了,理论讲得够多了。让我们看看如何在实践中使用 JSDoc。
在实践中使用 JSDoc
在项目中开始使用 JSDoc 时,会面临一些挑战。本节将重点介绍这些挑战以及如何克服它们。
如何充分利用 JSDoc
在本文中,我将坚持使用 VSCode。如果您正在使用其他编辑器,仍然可以继续学习,但您可能需要了解如何在您的编辑器中进行配置。
VSCode 内置了对 JSDoc 的支持。这意味着您无需安装任何其他扩展即可获得 JSDoc 的诸多优势。此外,您还可以通过一些操作来进一步发挥 JSDoc 的优势。在jsconfig.json
文件中启用 checkJs 选项,编辑器会显示类型不匹配的错误,即使在 JavaScript 文件中也是如此。请将其放置在项目的根目录或您想要启用类型检查的文件夹中。此文件如下所示:
{
"compilerOptions": {
"checkJs": true,
}
}
要将此选项应用于所有项目,请按 访问 VSCode 设置cmd + ,
,搜索 checkJs 并在那里启用它。如果需要更严格的类型检查,请考虑在 jsconfig 中启用其他选项,例如strict
和noImplicitAny
。
strict
强制执行一组更严格的类型检查规则,这有助于识别代码中的潜在问题。启用此选项后,截至撰写本文时,以下与类型相关的标志均设置为 true:
- noImplicitAny:当表达式或声明隐含 any 类型时,会报错。如果没有指定变量的类型,变量会被推断为 any,从而报错。
- noImplicitThis:如果 TypeScript 无法确定 this 的类型,则会报错。
- alwaysStrict:将所有文件视为在文件顶部具有严格模式指令(“use strict”)。
- 以及其他选项,如strictBindCallApply、strictNullChecks、strictFunctionTypes、strictPropertyInitialization、useUnknownInCatchVariables。
您可以在TypeScript 文档中阅读有关这些选项的更多信息。
通常,您只想启用其中一部分选项。您可以通过启用strict
然后禁用不想使用的选项来实现。例如,如果您只想启用strictNullChecks
但不想使用strictFunctionTypes
,则可以在 jsconfig 中启用strict
然后禁用strictFunctionTypes
。根据您的用例,您可能还需要启用其他一些相关选项:
allowUmdGlobalAccess
允许您访问 UMD 模块中的全局变量。我不会在这里详细介绍 JavaScript 模块,但您可以阅读 Igor Irianto 的这篇文章了解更多信息。简而言之,如果您正在使用 jQuery 或 Lodash 之类的库,并且希望在不导入的情况下分别访问它们的全局变量$
和,那么您很可能需要启用此选项。_
typeAcquisition
允许您指定要在项目中使用的库。然后,它将自动从DefinitelyTyped项目下载这些库的类型定义。此社区项目包含一些 npm 软件包的类型定义,这些软件包本身没有附带类型定义。它看起来可能像这样:
{
"compilerOptions": {
"typeAcquisition": {
"include": ["jquery", "lodash"]
}
}
}
.d.ts 文件
TypeScript 使用.d.ts
文件来存储类型定义。这些文件通常用于为未附带类型定义的 JavaScript 库定义类型。您也可以使用它们来定义您自己的 JavaScript 代码的类型。以下是文件的示例.d.ts
:
declare const foo: string;
declare function bar(): User;
declare class Baz {}
interface User {
name: string;
age?: number;
}
以下是如何在 JavaScript 代码中使用它:
foo; // string
bar(); // User
new Baz(); // Baz
在.d.ts
文件中,您可以使用我们之前介绍过的所有 TypeScript 功能,甚至更多。TypeScript 会自动选择您的.d.ts
文件以及您安装的 npm 包中的文件。实际上,您可以在要添加类型的 JavaScript 文件附近创建该文件。对于全局类型,您可以globals.d.ts
在项目根目录中创建一个名为 的文件,并将其添加到那里。
在 JavaScript 中,有两种方法可以从.d.ts
文件导入类型。第一种方法是使用三斜杠指令。这些指令会告诉 TypeScript 包含来自指定模块的类型定义。它可能如下所示:
// If you want to use a .d.ts file
/// <reference path="./foo.d.ts" />
// If you want to use jQuery
/// <reference types="jquery" />
// If you want to use es2017 string features like .padStart()
/// <reference lib="es2017.string" />
有关三斜杠指令的更多信息,请参阅TypeScript 文档。
第二种方法是使用import
关键字。这将从指定的模块导入类型定义。以下是示例:
/** @typedef {import('./foo.d.ts').Foo} Foo */
/** @typedef {import('type-fest').JsonValue} JsonValue */
在最后一章中,我想分享一些编写 JSDoc 注释的最佳实践。我还会分享一些资源,供您进一步了解 JSDoc 和 TypeScript。
最佳实践
代码文档的详细程度取决于具体的用例、项目规模和受众。在提供足够的信息帮助用户理解代码和避免混乱之间取得平衡至关重要。以下是一些您可以参考的最佳实践:
-
考虑你的受众:如果你正在开发一个库,你的文档应该全面,并包含所有类型、函数和接口的详细描述。这有助于库的用户了解如何有效地使用它。另一方面,如果你正在与规模较小的团队合作开展内部项目,你可以选择专注于高级解释和重要的边缘案例。
-
保持注释更新:随着代码的演进,请务必更新相应的注释和文档。过时的注释可能会产生误导,并给使用您代码的开发人员带来困惑。
-
简洁明了:力求在注释中提供简洁明了的解释。避免使用过于专业的术语,并专注于提供易于理解的信息。请记住,您的文档应该对经验丰富的开发人员和新手都有所帮助。
-
包含代码示例:在适当的情况下,添加代码示例来说明如何使用特定函数或类型。这对于不熟悉代码库或其涉及概念的用户尤其有用。
-
遵循一致的风格:对您的注释和文档使用一致的风格。这有助于创建具有凝聚力和专业性的外观,使用户更容易阅读和理解您的文档。
如果你已经读到这里,那就为你点赞吧!很高兴你今天学到了新东西。现在你可以开始在 JavaScript 代码中添加 JSDoc 注释,让它变得几乎像 TypeScript 一样🎉。你可以关注我并留言支持我。我很乐意听到你的想法和反馈。
其他资源
- JSDoc 文档
- TypeScripts JSDoc 文档
- TypeScript 路线图
- JavaScript 路线图
- Javascript 中的 CJS、AMD、UMD 和 ESM 到底是什么?作者:Igor Irianto
- 现代 JavaScript 教程