使用JSDoc代替一部分的Typescript

发布时间:2023-11-29 15:02
最后更新:2023-11-30 15:16
所属分类:
前端 React

在几乎Typescript已经成功统治前端领域的今天,做为前端人,你是已经习惯了Typescript的类型声明,还是依旧对Typescript怀抱抵触心理?而且网络上各种“去Typescipt”的言论和行动此起彼伏,从来都没有断绝的意思。

不过我也相信很多人在使用一段时间Typescript以后,都会产生一个疑问:Typescript到底给我带来了什么好处?

当然,使用Typescript编写项目的确是有很多好处的。严格的类型定义,可以在编译过程中对代码进行检查和优化等等。但是很多时候我们依旧会质疑一下,项目里是不是真的需要这些特性。当然,可能Typescript里最让你头疼的可能是复杂的类型体操,我想如果你打算回到Javascript的话,大概是类型体操已经做够了。

不过,等你真的回到了Javascript,你就会开始怀念Typescript里定义的那些类型了,配合上各个编辑器和IDE的自动提示,那是真的香。而这些工具在分析Javascript的时候,就像是一个傻子一样,一直在提示它们的类型都是any。这尤其对打算写一个工具函数或者比较公共一点儿的组件就很不友好了。

现在,我们有了一个可以在Javascript里继续做类型体操的方法:JSDoc。

JSDoc可以做什么

一看到JSDoc这个名称,大部分人的第一反应就是这是用来写注释的。没错,JSDoc的确是用来书写Javascript代码注释的,但是在目前的几版加强以后,JSDoc也可以用来声明类型了。所以相比于Typescript,JSDoc是一个足够轻量的仅仅用来辅助类型声明的工具,然而在大部分项目的开发过程中,这一个功能就已经足够了。

如何编写JSDoc

既然是注释,那么JSDoc就是生存在注释里的。JSDoc要求被书写在使用/** */的注释环境中,不论这个注释是多行的还是只有一行。在后面的示例中可以看到,书写在一行中的JSDoc和书写在多行中的JSDoc的区别。

注释一般都是文本内容,最多使用HTML或者Markdown来编写格式更加丰富的内容。JSDoc为了进一步丰富注释中的内容,就引入了标签(tag)。标签可以使用一定的格式声明更加丰富的内容,让注释更加清晰有力。

以下是一个我们常见的使用JSDoc注释一个Javascript函数的示例。

1
2
3
4
5
6
7
8
9
/**
 * 执行 a + b 的功能,返回两个参数的和。
 * @param {number} a 参数a
 * @param {number} b 参数b
 * @returns {number} 两个参数的和
 */
function add(a, b) {
  return a + b;
}

常见的类型声明方法

本文不打算逐一的说明JSDoc里各个标签的使用方法了,而是采用结合日常项目中经常出现的类型来完成各个常用标签的简要说明。为了能够更容易的理解,这些类型声明都将与Typescript中的等价声明一起做对照。

基本类型的变量

基本类型的变量是使用@type来声明其类型的,格式为@type {type}

在JSDoc中声明使用的类型都是使用{}包裹的,在后面的示例中也可以清楚的看到这一点。

以下是Typescript中的变量类型声明方式。

1
2
3
4
5
const name: string = "Kate";
const age: number = 24;
const isActive: boolean = true;
const nullable: number | null = null;
const unassigned: string | undefined;

相应的在Javascript中使用JSDoc就变成了这样。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/** @type {string} */
const name = "Kate";
/** @type {number} */
const age = 24;
/** @type {boolean} */
const isActive = true;
/** @type {?number} */
const nullable = null;
/**
 * 可空的变量还可以使用Union类型来声明。
 * @type {number | null}
 */
const nullable = null;
/** @type {string | undefined} */
const unassigned;

对于可空、不可空等类型的声明,JSDoc里提供了一些与Typescript中不同的标识方法。

  • {?Type}标识一个可空类型,相当于Type | null
  • {!Type}标识一个不可空类型,这个类型不允许获取null值。
  • {...Type}标识一个可以接受可变数量内容的变量,通常用在@param标签里,用于声明函数中的可变参数。
在成块的书写注释的时候,被注释内容的类型声明需要紧贴着被注释的内容书写,也就是这一块注释的尾部。如果需要声明多个类型,那么可以多划分几段注释。

数组类型和元组类型

在Typescript中声明一个数组类型就是十分简单的使用[]或者使用数组的泛型形式Array<T>即可,这个类型表示方法在JSDoc中也是可以用的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 在Typescript中
const numbers: number[] = [1, 2, 3];
const numbers: Array<number> = [1, 2, 3];
const matrix: number[][] = [[1, 2], [3, 4]];
const matrix: Array<Array<number>> = [[1, 2], [3, 4]];

// 在Javascript中
/** @type {number[]} */
const numbers = [1, 2, 3];
/** @type {Array<number>} */
const numbers = [1, 2, 3];
/** @type {number[][]} */
const matrix = [[1, 2], [3, 4]];
/** @type {Array<Array<number>>} */
const matrix = [[1, 2], [3, 4]];

元组则是一组保存在同一个数组中的不同类型的元素。

1
2
3
4
5
6
7
8
9
// 在Typescript中
const coordinate: [number, number] = [30, 30];
const flight: [string, number] = ["FA37", 780];

// 在Javascript中
/** @type {[number, number]} */
const coordinate = [30, 30];
/** @type {[string, number]} */
const flight = ["FA37", 780];

声明对象和接口类型

在Typescript里可以直接使用对象的构成来定义个匿名的对象类型,这在JSDoc里也是可以的。

1
2
3
4
5
6
7
8
9
// 在Typescript中
const user: { name: string, privileges: string[] } = {
  name: "John",
  privileges: ["root"],
};

// 在Javascript中
/** @type {{ name: string, privileges: string[] }} */
const user = { name: "John", privileges: ["root"] };

但是如果是在Typescript中使用type关键字定义一个类型或者是使用interface关键字定义一个接口,那么在JSDoc里就不是那么容易办到了,但是也并不是没有办法。JSDoc里提供了@typedef标签可以用来直接定义一个类型的别名,然后可以搭配@property标签就可以定义其字段。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 在Typescript中
interface User {
  name: srting;
  priviliegs: string[];
  enabled: boolean;
  birth?: string;
}
const user: User = {
  name: "John",
  privileges: ["root"],
  enabled: true,
};

// 在Javascript中
/**
 * @typedef {Object} User
 * @property {string} name
 * @property {string[]} privileges
 * @property {boolean} enabled
 * @property {string} [birth]
 */
/** @type {User} */
const user = {
  name: "John",
  privileges: ["root"],
  enabled: true,
};

在上面的示例中,JSDoc通过紧跟在@typedef@class等标签后面的@property标签来定义其中的字段。Typescript中通过?定义了一个可选字段,这个可选字段在JSDoc中则是通过[]包裹字段的名称来实现的。

@typedef标签实际上的功能是定义一个类型的别名,用来定义类型别名的类型可以是任意类型。

定义枚举类型

Typescript中提供了一个enum关键字来定义枚举,例如:

1
2
3
4
5
6
7
enum Colors {
  Red = "#ff0000",
  Green = "#00ff00",
  Blue = "#0000ff",
};

const red: Colors = Colors.Red;

但是Javascript中实际上是没有提供枚举类型的,枚举类型在Javascript中实际上是使用普通的对象来实现的,在Typescript里可以表示为Record<string, string>。JSDoc里提供了@enum {type}标签来定义枚举,其中需要声明的类型是值的类型。

例如上面的示例可以使用JSDoc改写为以下样子。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/**
 * @enum {string}
 */
const Colors = {
  Red: "#ff0000",
  Green: "#00ff00",
  Blue: "#0000ff",
};

/** @type {Colors} */
const red = Colors.Red;

联合类型

联合类型是Typescript中使用的比较多的一种复合类型,通常用来使变量可以接受多种类型的值,或者是指定变量可以使用的字面量取值范围。

例如以下两种联合类型的示例。

1
2
3
4
5
type StringOrNumber = string | number;
type Color = 'red' | 'green' | 'blue';

const greeting: StringOrNumber = 'hello';
const bgColor: Color = 'green';

这种联合类型在JSDoc中实际上声明的语法也基本上是一样的。

1
2
3
4
5
6
/** @typedef {string | number} StringOrNumber */
/** @typedef {'red' | 'green' | 'blue'} Colors */
/** @type {StringOrNumber} */
const greeting = 'hello';
/** @type {Colors} */
const bgColor = 'green';

工具类型

工具类型是Typescript里提供的用来利用现有的类型生成新类型的,就像是一组工具方法一样,比如常用的PartialReadonlyRecordPickOmit等等。这些工具类型在JSDoc里也同样获得了支持,可以直接使用。

例如以下常见的工具类型使用示例。

1
2
3
4
5
6
7
8
9
interface User {
  name: string;
  age: number;
}

type PartialUser = Partial<User>;
type ReadonlyUser = Readonly<User>;
type UserName = Pick<User, 'name'>;
type UserAge = Omit<User, 'name'>;

这些类型如果使用JSDoc来声明,就是下面的样子。

1
2
3
4
5
/** @typedef {{ name: string; age: number; }} User */
/** @typedef {Partial<User>} PartialUser */
/** @typedef {Readonly<User>} ReadonlyUser */
/** @typedef {Pick<User, 'name'>} UserName */
/** @typedef {Omit<User, 'name'>} UserAge */
出了可以使用Pick工具类型从一个类型中拆分出其某个字段的类型以外,Typescript中经常使用的User['name']获取指定字段类型的方式也是可以使用的。

使用泛型

其实泛型在之前的示例中已经使用过了,但是还没有真正的使用过一个类型变量。泛型在Typescript中使用起来是十分容易的。

1
2
3
4
type TypeT<T> = T;
type TypeTOrU<T, U> = T | U;
type TypeBoolean = TypeT<boolean>;
type TypeStringOrNumber = TypeTOrU<string, number>;

在JSDoc中使用泛型,就需要使用JSDoc提供的@template标签来定义所要使用的类型模板了。@template标签不使用{}包裹被声明的类型变量,而且@template标签需要放置在所有使用到类型变量的位置前面。

例如上面的示例改写之后就是下面的样子。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/**
 * @template T
 * @typedef {T} TypeT
 */
/**
 * @template T, U
 * @typedef {T | U} TypeTOrU
 */
/** @typedef {TypeT<boolean>} TypeBoolean */
/** @typedef {TypeTOrU<string, number>} TypeStringOrNumber */

定义函数类型

函数类型在Typescript中一般都是通过Lambda表达式形式的函数签名体来定义的,例如() => void。但是在JSDoc中提供了可以直接声明函数的标签@function,而且与@function标签配合的还有一组用于描述函数定义的标签。

  • @function用于定义一个函数,可以指定函数类型的名称。
  • @param用于定义函数的一个参数,格式为@param {Type} 参数名 参数描述
  • @returns用于定义函数的返回值类型,格式为@param {Type} 返回值描述
  • @throws用于定义函数可能抛出的异常类型,格式为@throws {Type} 异常描述
  • @generator用于标注在@function之前,声明当前的函数是一个生成器函数。
  • @yield用于配合@generator标注,声明生成器函数抛出的数据类型,格式为@yield {Type}

映射类型

映射类型允许通过目前已有的类型创建一个新的类型,这种定义通常会形成一个十分类似于map的类型。其实这种映射类型的舍命就已经开始有一些类型体操的意味了。

例如在Typescript中定义一个Nullable<T>类型。

1
2
3
type Nullable<T> = {
  [P in keyof T]: T[P] | null;
};

其实在JSDoc里,定义这样的一个映射类型,语法也是基本一样的。

1
2
3
4
/**
 * @template T
 * @typedef {{ [P in keyof T]: T[P] | null }} Nullable<T>
 */
使用与Typescript基本一致的语法的还有条件类型,例如type IsString<T> = T extends string ? 'T' : 'F',在JSDoc里就是/** @typedef {T extends string ? 'T' : 'F'} IsString<T>,基本上就是一模一样的。

引用其他文件中定义的类型

在Typescript中如果需要使用一个在其他文件中声明的类型,那么首先就需要在声明这个类型的文件中将声明的类型export,然后在需要使用这个类型的文件中import

例如有这样一个文件types.ts

1
2
3
4
export type User = {
  name: string;
  age: number;
}

如果要在其他文件中使用,就必须这样做。

1
2
3
import type { User } from './types.ts';

type UserName = Pick<User, 'name'>;

但是使用JSDoc的时候就不必这样,JSDoc默认会导出所有的类型定义,可以直接导入使用,只是在导入的时候需要做一个类型别名。例如同样有这样一个类型定义文件types.js

1
2
3
4
5
/**
 * @typedef {object} User
 * @property {string} name
 * @property {number} age
 */

那么在使用这个类型的文件里就可以这样来引用它。

1
2
3
/**
 * @typedef {import('./types.js').User} User
 */

JSDoc标签小结

这篇文章中可能没有涉及到所有的JSDoc标签,也没有涉及到所有常用的标签,所以在这里对常用于声明类型的JSDoc标签做一个列表概要说明。

标签 功能
@type 定义一个变量的类型。
@typedef 定义一个类型的别名。
@property 定义一个对象中的一个字段。
@template 定义一个或一组泛型变量。
@enum 定义一个枚举类型。
@function 定义一个函数。
@class 定义一个类的构造函数,这个函数可以使用new关键字调用,Javascript里的类也是使用function定义的。
@generator 声明一个函数为生成器函数。
@yield 声明一个生成器函数抛出的值的类型。
@async 声明一个函数为异步函数。
@param 定义函数的一个参数。
@returns 定义函数的返回值。
@throws 定义函数可能抛出的异常。
@constant 定义一个常量。
@constructs 声明函数是前一个@class的构造函数。
@default 声明默认值,常配合@constant或者@type使用。
@interface 声明一个接口,在Javascript里接口就是一个空函数,但是在原型上定义需要实现的内容。
@implements 声明所实现的接口,跟接口一样,同样需要在原型上定义实现。
@event 声明函数中可能会触发的事件,事件名称一般使用类名#命名空间:事件名的形式,其中命名空间的名称可以省略。
@fires 声明函数中将会触发的时间类型,事件的命名形式与@event标签相同。
@namespace 声明一个命名空间,可以认为是一个用于容纳其他对象的对象, 也可以用于声明虚对象。
@memberof 声明所属的命名空间,常用于内容和命名空间分别定义时。
@readonly 声明一个只读的字段。
@abstract 声明一个虚方法。
@access 声明一个成员的可访问级别,可取值packageprivateprotectedpublic
@private 声明命名空间中的一个成员为私有。
@public 声明命名空间中的一个成员为公有。
@protected 声明命名空间里的一个成员为子类可访问。

索引标签
JSDoc
Typescript
Tags
类型声明