1. 如何查看当前包是否有现成的声明文件?
在我们尝试给一个 npm 包创建声明文件之前,需要先看看它的声明文件是否已经存在。一般来说,npm 包的声明文件可能存在于两个地方:
- 与该 npm 包绑定在一起。判断依据是
package.json
中有types
字段,或者有一个index.d.ts
声明文件。这种模式不需要额外安装其他包,是最为推荐的,所以以后我们自己创建 npm 包的时候,最好也将声明文件与 npm 包绑定在一起。 - 发布到
@types
里。我们只需要尝试安装一下对应的@types
包就知道是否存在该声明文件,安装命令是npm install @types/foo --save-dev
。这种模式一般是由于 npm 包的维护者没有提供声明文件,所以只能由其他人将声明文件发布到@types
里了。
2. 自己编写的声明文件放在哪儿?
假如以上两种方式都没有找到对应的声明文件,那么我们就需要自己为它写声明文件了。由于是通过 import
语句导入的模块,所以声明文件存放的位置也有所约束,一般有两种方案:
- 创建一个
node_modules/@types/foo/index.d.ts
文件,存放foo
模块的声明文件。这种方式不需要额外的配置,但是node_modules
目录不稳定,代码也没有被保存到仓库中,无法回溯版本,有不小心被删除的风险,故不太建议用这种方案,一般只用作临时测试。 - 创建一个
types
目录,专门用来管理自己写的声明文件,将foo
的声明文件放到types/foo/index.d.ts
中。这种方式需要配置下tsconfig.json
中的paths
和baseUrl
字段。
目录结构:
1 | /path/to/project |
tsconfig.json
内容:
1 | { |
如此配置之后,通过 import
导入 foo
的时候,也会去 types
目录下寻找对应的模块的声明文件了。
3. 编写声明文件
npm 包的声明文件主要有以下几种语法:
export
导出变量export namespace
导出(含有子属性的)对象export default
ES6 默认导出export =
commonjs 导出模块
export
§
npm 包的声明文件与全局变量的声明文件有很大区别。在 npm 包的声明文件中,使用 declare
不再会声明一个全局变量,而只会在当前文件中声明一个局部变量。只有在声明文件中使用 export
导出,然后在使用方 import
导入后,才会应用到这些类型声明。
export
的语法与普通的 ts 中的语法类似,区别仅在于声明文件中禁止定义具体的实现15:
1 | // types/foo/index.d.ts |
对应的导入和使用模块应该是这样:
1 | // src/index.ts |
混用 declare
和 export
§
我们也可以使用 declare
先声明多个变量,最后再用 export
一次性导出。上例的声明文件可以等价的改写为16:
1 | // types/foo/index.d.ts |
注意,与全局变量的声明文件类似,interface
前是不需要 declare
的。
export namespace
§
与 declare namespace
类似,export namespace
用来导出一个拥有子属性的对象17:
1 | // types/foo/index.d.ts |
1 | // src/index.ts |
export default
§
在 ES6 模块系统中,使用 export default
可以导出一个默认值,使用方可以用 import foo from 'foo'
而不是 import { foo } from 'foo'
来导入这个默认值。
在类型声明文件中,export default
用来导出默认值的类型18:
1 | // types/foo/index.d.ts |
1 | // src/index.ts |
注意,只有 function
、class
和 interface
可以直接默认导出,其他的变量需要先定义出来,再默认导出19:
1 | // types/foo/index.d.ts |
上例中 export default enum
是错误的语法,需要使用 declare enum
定义出来,然后使用 export default
导出:
1 | // types/foo/index.d.ts |
针对这种默认导出,我们一般会将导出语句放在整个声明文件的最前面20:
1 | // types/foo/index.d.ts |
export =
§
在 commonjs 规范中,我们用以下方式来导出一个模块:
1 | // 整体导出 |
在 ts 中,针对这种模块导出,有多种方式可以导入,第一种方式是 const ... = require
:
1 | // 整体导入 |
第二种方式是 import ... from
,注意针对整体导出,需要使用 import * as
来导入:
1 | // 整体导入 |
第三种方式是 import ... require
,这也是 ts 官方推荐的方式:
1 | // 整体导入 |
对于这种使用 commonjs 规范的库,假如要为它写类型声明文件的话,就需要使用到 export =
这种语法了21:
1 | // types/foo/index.d.ts |
需要注意的是,上例中使用了 export =
之后,就不能再单个导出 export { bar }
了。所以我们通过声明合并,使用 declare namespace foo
来将 bar
合并到 foo
里。
准确地讲,export =
不仅可以用在声明文件中,也可以用在普通的 ts 文件中。实际上,import ... require
和 export =
都是 ts 为了兼容 AMD 规范和 commonjs 规范而创立的新语法,由于并不常用也不推荐使用,所以这里就不详细介绍了,感兴趣的可以看官方文档。
由于很多第三方库是 commonjs 规范的,所以声明文件也就不得不用到 export =
这种语法了。但是还是需要再强调下,相比与 export =
,我们更推荐使用 ES6 标准的 export default
和 export
。