HOW - Typescript 类型声明文件

一、背景

随着 Typescript 的不断发展,很多流行的公共库(譬如 react、Lodash、axios)都为自己编写了类型文件,以便 TypeScript 用户能够更好地使用它们的 API。

在使用这些库时,你可以通过安装它们的 npm 包来自动获取相应的类型定义文件,或者手动下载和管理类型定义文件,以便在 TypeScript 项目中正确地使用这些库的 API。

总之,为你的 npm 包添加类型定义可以让使用方在编码时获得更好的 IDE 支持和代码提示,提高代码的可读性和可维护性。

二、如何添加类型定义支持智能提示

下面我将介绍两种常见的方法来为 npm 工具包添加类型定义:使用 TypeScript 编写类型声明文件和使用 JSDoc 注释。

方法一:使用 JSDoc 注释

JSDoc 是 JavaScript 的一种文档注释格式,用于描述代码的结构、功能和类型信息。它允许开发者在代码中使用特定的注释语法来提供对函数、变量、类等元素的描述,以及参数类型、返回值类型等信息。JSDoc 注释以 /** ... */ 的形式出现在代码的顶部,通常用于生成文档和提供代码提示。

以下是一个简单的 JSDoc 注释的示例:

/**
 * 计算两个数的和
 * @param {number} a 第一个数字
 * @param {number} b 第二个数字
 * @returns {number} 两个数的和
 */
function add(a, b) {
  return a + b;
}

在这个示例中,我们使用 JSDoc 注释来描述了 add 函数的功能和参数信息。具体来说:

  • @param {number} a:指定了 a 参数的类型为 number,表示第一个数字。
  • @param {number} b:指定了 b 参数的类型为 number,表示第二个数字。
  • @returns {number}:指定了函数的返回值类型为 number,表示两个数字的和。

当然,除了函数之外,JSDoc 注释也可以用于注释其他类型的代码元素,比如变量、类、方法、属性等。示例:

  1. 变量
/**
 * 表示人的姓名
 * @type {string}
 */
const name = 'John';
/**
 * 表示一个人的类
 * @class
 */
class Person {
  /**
   * 创建一个人的实例
   * @param {string} name 人的姓名
   */
  constructor(name) {
    this.name = name;
  }
  
  /**
   * 获取人的姓名
   * @returns {string} 人的姓名
   */
  getName() {
    return this.name;
  }
}
  1. 属性
/**
 * 表示一个人的年龄
 * @type {number}
 */
Person.prototype.age = 30;

这些是一些常见的其他类型代码元素的 JSDoc 注释示例。通过使用 JSDoc 注释来描述代码的结构、功能和类型信息,可以使代码更易于理解和维护,并且提供更好的 IDE 支持和代码提示。

通过使用 JSDoc 注释,开发者可以提供代码的清晰文档,同时帮助 IDE 和其他工具生成更好的代码提示和自动补全,提高开发效率。JSDoc 注释可以与各种 JavaScript 开发工具和框架一起使用,包括 Visual Studio Code、Eclipse、WebStorm 等。

另外,还记得我们在 Webpack 详解系列(二) 中介绍的“提升 Webpack 配置体验:类型智能提示”吗?

因为 Webpack 的配置项比较多,而且很多选项都支持不同类型的配置方式。如果你刚刚接触 Webpack 的配置,这些配置选项一定会让你感到头大。如果开发工具能够为 Webpack 配置文件提供智能提示的话,这种痛苦就会减小很多,配置起来,效率和准确度也会大大提高。

我们知道, VSCode 对于代码的自动提示是根据成员的类型推断出来的,换句话说,如果 VSCode 知道当前变量的类型,就可以给出正确的智能提示。即便你没有使用 TypeScript[1] 这种类型友好的语言,也可以通过类型注释[2]的方式去标注变量的类型。

默认 VSCode 并不知道 Webpack 配置对象的类型,我们可以通过 import 的方式导入 Webpack 模块中的 Configuration 类型,@type {Configuration},然后根据类型注释的方式将变量标注为这个类型,这样我们在编写这个对象的内部结构时就可以有正确的智能提示了,具体代码如下所示:

// ./webpack.config.js
import { Configuration } from 'webpack'

/**
 * @type {Configuration}
 */
const config = {
    // ...
}
module.exports = config

注意:我们添加的 import 语句只是为了导入 Webpack 配置对象的类型,这样做的目的是为了标注 config 对象的类型,从而实现智能提示。在配置完成后,一定要记得运行 Webpack 之前注释掉这段辅助代码,因为在 Node.js 环境中默认还不支持 import 语句,如果执行这段代码会出现错误。

但手动注释终究不便,更一般的做法是直接在类型注释中使用 import 动态导入类型,@type {import('webpack').Configuration},具体代码如下:

// ./webpack.config.js
/** @type {import('webpack').Configuration} */
const config = {
    // ...
}
module.exports = config

这种方式同样也可以实现载入类型,而且相比于在代码中通过 import 语句导入类型更为方便,也更为合理。

注意,这种导入类型的方式并不是 ES Modules 中的 Dynamic Imports,而是 TypeScript 中提供特性。

虽然我们这里只是一个 JavaScript 文件,但是在 VSCode 中的类型系统都是基于 TypeScript 的,所以可以直接按照这种方式使用,详细信息你可以参考这种 import-types 的文档。

方法二:使用 TypeScript 编写类型声明文件 .d.ts

.d.ts 文件是 TypeScript 类型声明文件的常见文件扩展名。在 TypeScript 中,类型声明文件用于描述 JavaScript 代码中的类型信息,包括变量、函数、类等的类型定义。.d.ts 文件通常以纯文本形式存在,其中包含了 TypeScript 类型声明的定义,但是不包含实际的代码实现。

例如:

// 引入第三方库的类型声明文件
import { SomeLibraryType } from 'some-library';
// 使用第三方库的类型
const instance: SomeLibraryType = new SomeLibraryType();

.d.ts 文件可以手动编写,也可以通过工具自动生成。无论是手动编写还是自动生成,.d.ts 文件的目的都是为了提供对 JavaScript 代码的类型信息描述,以帮助 TypeScript 编译器和 IDE 在代码开发过程中提供更好的支持和提示。

主要流程如下:

  1. 创建声明文件:在你的 npm 工具包项目中创建一个新的 TypeScript 文件,通常命名为 index.d.ts,用于存放类型声明。在该文件中,你可以使用 TypeScript 语法编写对应的类型声明。

  2. 编写类型声明:根据你的工具包提供的 API,编写对应的类型声明。你可以使用 TypeScript 的接口(interface)、类型别名(type alias)等语法来描述函数、类、对象等的类型结构。

  3. 发布声明文件:在发布 npm 包时,确保将 TypeScript 声明文件一起发布到 npm 仓库。通常情况下,当你通过 npm publish 发布包时,会自动包含 index.d.ts 文件。

在后面我们会详细介绍该部分。

方法三:JSDoc 注释转 Typescript 类型声明文件

要将项目中编写的 JSDoc 注释转换为 TypeScript 类型声明文件,可以使用一些工具来辅助完成这个过程。下面是一种常见的方法:

  1. 使用 dts-gen 工具

dts-gen 是一个由 Microsoft 提供的工具,可以根据 JavaScript 文件中的 JSDoc 注释生成 TypeScript 类型声明文件。以下是使用步骤:

  • 首先,确保你的项目中已经编写了详细的 JSDoc 注释。
  • 然后,安装 dts-gen 工具:
npm install -g dts-gen
  • 接下来,在项目根目录下运行 dts-gen 命令并指定要生成类型声明文件的 JavaScript 文件:
dts-gen your-js-file.js

这将会在当前目录下生成一个 your-js-file.d.ts 文件,其中包含了根据 JSDoc 注释生成的 TypeScript 类型声明。

dts-gen 工具会尽可能地根据 JSDoc 注释来推断类型,但是它可能无法覆盖所有情况,需要手动检查并调整生成的类型声明文件。

  1. 手动编写类型声明文件

如果你的项目中的 JSDoc 注释比较简单或者 dts-gen 无法满足你的需求,你也可以手动编写 TypeScript 类型声明文件。在这种情况下,你需要根据项目中的 JSDoc 注释来逐个添加相应的类型声明,确保类型声明文件的准确性和完整性。

总结

无论你选择哪种方法,都要确保类型声明的准确性和完整性,并且及时更新和维护类型定义,以反映你工具包的最新 API 和功能。这样可以帮助使用方更好地理解和使用你的 npm 工具包。

二、使用 TypeScript 编写类型声明文件

前面提到主要流程包括:

  1. 创建声明文件
  2. 编写类型声明
  3. 发布声明文件

假设我们有一个 JavaScript 工具包,它提供了一个请求拦截器函数 interceptRequest,用于添加 Header 信息到 HTTP 请求中。现在我们要为这个工具包编写 TypeScript 类型声明文件。

1. 创建类型声明文件

首先,我们需要创建一个 TypeScript 类型声明文件,通常命名为 index.d.ts,并且与 JavaScript 工具包放在同一个目录下。

// index.d.ts
declare module 'example-toolbox' {
  export function interceptRequest(headers: Record<string, string>): void;
}

在这个声明文件中,我们使用 declare module 来声明一个模块,并指定模块的名称为 'example-toolbox',这个名称应该与你的 JavaScript 工具包的名称相匹配。然后我们使用 export function 来导出 interceptRequest 函数,并指定其参数类型为 Record<string, string>

2. 编写类型声明

接下来,我们编写类型声明文件中的具体内容,即对工具包提供的函数和参数进行描述。在这个例子中,我们只需要描述 interceptRequest 函数的参数即可。

不过,在实际应用中,我们需要编写更完善、更复杂的类型定义,除了在 Typescript 常用特性介绍 里我们介绍了许多特性的用法,下面还需要学习一些关于编写类型文件需要的前置知识:

2.1 declare

在 TypeScript 中,declare 关键字用于声明一些全局性的类型信息,而不生成实际的 JavaScript 代码。

declare 的主要用途是在类型声明文件(.d.ts 文件)中为已有的 JavaScript 代码提供类型信息,以便在 TypeScript 项目中使用。

以下是一些使用 declare 的常见场景:

  1. 声明全局变量

如果在项目中有一些全局变量,可以使用 declare 关键字为它们提供类型信息。

// global.d.ts
declare const globalVar: string;

现在,可以在项目中安全地使用 globalVar 变量,而 TypeScript 会知道它的类型。

  1. 声明全局函数

如果有一个全局函数,可以使用 declare 关键字为其提供类型信息。

// global.d.ts
declare function globalFunction(param: string): number;
  1. 声明全局类

与全局变量和全局函数类似,如果有一个全局类,可以使用 declare 关键字为其提供类型信息。

// global.d.ts
declare class GlobalClass {
  constructor(param: string);
  method(): void;
}
  1. 声明模块

当需要为一个没有类型声明的 JavaScript 库提供类型信息时,可以使用 declare module

// my-js-library.d.ts
declare module 'my-js-library' {
  export function myFunction(): string;
  export interface MyInterface {
    property: number;
  }
}

前面请求拦截的示例与此类似。

  1. 声明命名空间

如果需要将类型、接口和函数组织到一个命名空间中,可以使用 declare namespace

// my-namespace.d.ts
declare namespace MyNamespace {
  export function myFunction(): string;
  export interface MyInterface {
    property: number;
  }
}
// main.ts
const result = MyNamespace.myFunction();

declare namespacedeclare module 的用途和区别略有不同。

【用途】

declare namespace

  • 用途declare namespace 用于在全局作用域中声明一个命名空间,通常用于组织和封装相关的类型、接口、类、函数等。
  • 适用场景:适用于描述全局命名空间下的类型定义,比如在一个大型的应用程序中,可能会将各种类型定义放置在不同的命名空间下,以便更好地组织和管理代码。
  • 示例:常见的用法包括描述全局库、框架或应用程序的类型定义。

declare module

  • 用途declare module 用于在模块内部声明一个新的命名空间或模块,并且通常用于描述外部模块或第三方库的类型定义。
  • 适用场景:适用于描述外部模块或第三方库的类型定义,因为这些模块通常是通过 importrequire 导入的,而不是直接在全局命名空间中使用。
  • 示例:常见的用法包括描述 npm 包、浏览器库或其他外部模块的类型定义。

【区别】

  • 作用范围declare namespace 声明的命名空间是在全局作用域中的,而 declare module 声明的模块是在模块作用域中的。
  • 导入方式:对于 declare module,可以使用 importrequire 来导入模块的类型定义;而对于 declare namespace,其内部的类型定义是在全局作用域中的,不需要显式导入,可以直接使用。

2.2 模块化:导入导出

根据前面 export function interceptRequest 我们需要进一步学习 Typescript 的导入导出。

导入导出属于 Typescript 模块化。在 TypeScript 中,模块化代码是通过将代码拆分为多个文件来实现的,每个文件包含一组相关的函数、类和接口。使用模块化代码可以提高代码的可读性、可维护性和可重用性。

注意,每个文件应专注于一个特定的功能或实现一组相关的功能。

// models/person.ts
export interface Person {
  id: number;
  name: string;
}

// services/personService.ts
import { Person } from '../models/person';
export function getAllPersons(): Person[] {
  // ...
}

// main.ts
import { Person } from './models/person';
import { getAllPersons } from './services/personService';
const persons: Person[] = getAllPersons();
console.log(persons);

前面我们也使用 declare namespace 定义命名空间,它其实也属于模块化。只是,namespace 是 typescript 早期时为了解决模块化而创造的关键字。随着 ES6 的广泛应用,现在已经不建议再使用 typescript 中的 namespace,而推荐使用 ES6 的模块化方案

但是在在项目中 declare namespace 还是比较常用于全局命名空间场景的,避免名称冲突或污染。

3. 发布类型声明文件

最后,我们将 TypeScript 类型声明文件与 JavaScript 工具包一起发布到 npm 仓库。确保在发布时将 TypeScript 类型声明文件包含在内,并且在 package.json 中正确地指定了类型声明文件的入口。

// package.json
{
  "name": "example-toolbox",
  "version": "1.0.0",
  "description": "A JavaScript toolbox for intercepting requests",
  "main": "index.js",
  "types": "index.d.ts",
  // 其他属性...
}
# 登录到 npm 账号
npm login

# 发布包
npm publish

在发布完成后,其他开发者就可以通过 npm 安装你的工具包,并且在 TypeScript 项目中正确地使用它,享受到完善的类型提示和静态类型检查的好处。

4. 在 Typescript 项目中使用

其他开发者可以通过 npm install 安装你的库,并在 TypeScript 项目中直接使用你提供的类型。

可以直接在文件中引入:

import { interceptRequest } from 'example-toolbox';
// 创建一个请求头对象
const headers = {
  'Content-Type': 'application/json',
  'Authorization': 'Bearer token123'
};
// 使用工具包提供的函数添加请求头信息
interceptRequest(headers);

由于提供了类型声明文件,IDE 将会提供完善的代码提示和自动补全,帮助开发者更轻松地使用这个工具包提供的能力。

或者在 tsconfig.json 中配置。在 tsconfig.json 文件中,可以使用 types 配置项来引入类型声明文件。这样做的目的是告诉 TypeScript 编译器在编译时应该包含哪些类型声明文件。

假设你的工具包名为 example-toolbox,它的类型声明文件为 index.d.ts,你可以在 tsconfig.json 文件中像这样配置 types

{
  "compilerOptions": {
    "types": ["example-toolbox"]
  }
}

在这个配置中,types 是一个字符串数组,包含了你想要引入的类型声明文件的名称或模块名。这里我们指定了 example-toolbox,这意味着 TypeScript 编译器将会自动查找并引入 example-toolbox 模块下的类型声明文件。

注意,如果你的类型声明文件位于 node_modules/@types 目录下,你可以直接指定模块名而不需要提供文件路径。如果不在 node_modules/@types 目录下,则需要提供相对或绝对路径。

注意:在配置 types 时,不需要包含 .d.ts 文件的扩展名,只需指定模块名即可。

配置完成后,当你在项目中使用了 example-toolbox 模块时,TypeScript 编译器会自动引入对应的类型声明文件,以提供完整的类型检查和代码提示。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值