JavaScript 和 TypeScript 交叉口 —— 类型定义文件(*.d.ts)
在 《从 JavaScript 到 TypeScript 系列》 文章我们已经学习了 TypeScript 相关的知识。
TypeScript 的核心在于静态类型,我们在编写 TS 的时候会定义很多的类型,但是主流的库都是 JavaScript 编写的,并不支持类型系统。那么如何让这些第三方库也可以进行类型推导呢?
这篇文章我们来讲解 JavaScript 和 TypeScript 的静态类型交叉口 —— 类型定义文件。
前端开发 QQ 群:377786580
类型定义文件
在 TypeScript 中,我们可以很简单的,在代码编写中定义类型:
1 |
|
但是主流的库都是 JavaScript 编写的,TypeScript 身为 JavaScript 的超集,自然需要考虑到如何让 JS 库也能定义静态类型。
TypeScript 经过了一系列的摸索,先后提出了 tsd(已废弃)、typings(已废弃),最终在 TypeScript 2.0 的时候重新整理了类型定义,提出了 DefinitelyTyped。
DefinitelyTyped 就是让你把 “类型定义文件(*.d.ts)”,发布到 npm
中,配合编辑器(或插件),就能够检测到 JS 库中的静态类型。
类型定义文件的以 .d.ts
结尾,里面主要用来定义类型。
例如这是 jQuery 的类型定义文件 中一段代码(为了方便理解做了一些改动)
1 |
|
类型定义
*.d.ts
编写起来非常简单,经过 TypeScript 良好的静态类型系统洗礼过后,语法学习成本非常低。
我们可以使用 type
用来定义类型变量:
1 |
|
可以看到 type
其实可以定义各种格式的类型,也可以和其他类型进行组合。
1 |
|
当然,我们也可以使用 interface
定义我们的复杂类型,在 TS 中我们也可以直接定义 interface
:
1 |
|
interface
和 type
(或者说 class
) 很像。
但是 type
的含义是定义自定义类型,当 TS 提供给你的基础类型都不满足的时候,可以使用 type
自由组合出你的新类型,而 interface
应该是对外输出的接口。
type
不可以被继承,但 interface
可以:
1 |
|
declare
declare
可以创建 *.d.ts
文件中的变量,declare
只能作用域最外层:
1 |
|
基本上顶层的定义都需要使用 declare
, class
也是:
1 |
|
namespace
为防止类型重复,使用 namespace
用于划分区域块,分离重复的类型,顶层的 namespace
需要 declare
输出到外部环境,子命名空间不需要 declare
。
1 |
|
组合定义
上面我们只演示了一些简单的类型组合,生产环境中会包含许多复杂的类型定义,这时候我们就需要各种组合出强大的类型定义:
动态属性
有些类型的属性名是动态而未知的,例如:
1 |
|
我们可以使用动态属性名来定义类型:
1 |
|
类型遍历
当你已知某个类型范围的时候,可以使用 in
和 keyof
来遍历类型,例如上面的 ChinaMobile 例子,我们可以使用 in
来约束属性名必须为三家运营商之一:
1 |
|
我们也可以用 keyof
来约定方法的参数
1 |
|
import 导入和 export 导出
js 中有多种 import
和 export
模块的方式,所以在 d.ts
中也对应有着不同的导入导出方式。
导入规则
在 ts 中不同的 import
语法会决定如何解析这个 module:
- 有类型声明,标准 es module 库:
import * as xxxx from ''
或import xxx from ''
导入 - 有类型声明,标准 commonjs 库:
import xxx = require('')
导入 - 没有类型声明:
const xxx = require('')
导入(默认导入为 any 类型)
同样,js 中不同的 export
语法也对应着不同的声明方式。
export default
如果 js 是 es6 module
风格的,通过 export default
导出的,经过编译工具编译为 export.default
以后(一般为 babel 或其他工具编译),则声明文件这样写:
js 源文件:
1 |
|
d.ts 声明文件:
1 |
|
特点是这样引入(在 tsconfig.json
中没有开启 allowSyntheticDefaultImports
选项的情况下):
1 |
|
module.exports
如果 js 是 commonjs
风格的,是通过 module.exports
导出的,则声明文件这样写:
js 源文件:
1 |
|
d.ts 声明文件:
1 |
|
特点是这样引入:
1 |
|
export xxx
如果只是普通的 exports xxx
,经过编译工具编译为 export.xxx
之后,则声明文件相对简单:
js 源文件:
1 |
|
d.ts 声明文件:
1 |
|
引入方式:
1 |
|
集成发布
有两种主要方式用来发布类型定义文件到 npm
:
- 与你的 npm 包捆绑在一起(内置类型定义文件)
- 发布到 npm 上的 @types organization
前者,安装完了包之后会自动检测并识别类型定义文件。
后者,则需要通过 npm i @types/xxxx
安装,这就是我们前面所说的 DefinitelyTyped ,用于扩展 JS 库的类型声明。
内置类型定义文件
内置类型定义就是把你的类型定义文件和 npm 包一起发布,一般来说,类型定义文件都放在包根目录的 types
目录里,例如 vue:
如果你的包有一个主 .js
文件,需要在 package.json
里指定主类型定义文件。
设置 types
或 typeings
属性指向捆绑在一起的类型定义文件。 例如包目录如下:
1 |
|
1 |
|
如果主类型定义文件名是 index.d.ts
并且位置在包的根目录里,就不需要使用 types
属性指定了。
1 |
|
如果你发的包中,package.json
中使用了 files
字段的话(npm
会根据 files
配置的规则决定发布哪些文件),则需要手动把类型定义文件加入:
1 |
|
如果只发二级目录的话,把类型定义文件放到对应的二级目录下即可:
1 |
|
发布到 @types organizatio
发布到 @types organizatio
的包表示源包没有包含类型定义文件,第三方/或原作者定义好类型定义文件之后,发布到 @types 中。例如 @types/express。
根据 DefinitelyTyped
的规则,和编辑器(和插件) 自动检测静态类型。
@types 下面的包是从 DefinitelyTyped 里自动发布的,通过 types-publisher 工具。
如果想让你的包发布为 @types 包,需要提交一个 pull request 到 https://github.com/DefinitelyTyped/DefinitelyTyped。
在这里查看详细信息 contribution guidelines page。
如果你正在使用 TypeScript,而使用了一些 JS 包并没有对应的类型定义文件,可以编写一份然后提交到 @types
。
赠人玫瑰,手留余香。
发布到 @types organizatio
的包可以通过 TypeSearch 搜索检索,使用 npm install --save-dev @types/xxxx
安装:
更多细节请参阅 DefinitelyTyped。
其他
为第三方包声明类型
通常来说,如果这份类型定义文件是 JS 库自带的,那么我们可以直接导出模块:
1 |
|
而如果这份类型定义文件不是 JS 库自带的,而是第三方的,则需要使用 module
进行关联。
例如 jquery
发布的 npm 包中不包含 *.d.ts
类型定义文件,jquery
的类型定义文件发布在了 @types/jquery
,所以类型定义文件中导出类型的时候,需要关联模块 jquery
,意思就是我专门针对这个包做的类型定义:
1 |
|
从而解决了一些主流的 JS 库发布的 npm
包中没有类型定义文件,但是我们可以用第三方类型定义文件为这些库补充类型。
风格
经过一系列探索,个人比较推荐下面的编写风格,先看目录:
1 |
|
入口模块主要做这些事情:
- 定义命名空间
- 导出和聚合子模块
主出口文件 index.d.ts
:
1 |
|
子模块无需定义命名空间,这样外部环境 (types
文件夹之外) 则无法获取子模块类型,达到了类型封闭的效果:
1 |
|