当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
声明文件
新语法索引
declare var声明全局变量declare function申明全局函数declare class声明全局类declare enum声明全局枚举类型declare namespace声明(含有子属性的)全局对象interface和type声明全局类型export导出变量export namespace导出(含有子属性的)对象export defaultES5默认导出export =commonjs导出模块declare global扩展全局变量declare module扩展模块/// <reference>三斜杠指令
什么是声明语句
假设我们想使用第三方库jQuery,一种常见的方式是在Html中通过<script>标签映入jQuery,然后就可以使用全局变量$和jQuery了。 我们通过这样取一个id是foo的元素:
$('#foo') //找不到名称 "$"。是否需要安装 jQuery 的类型定义? 请尝试 `npm i @types/jquery`。t但是在ts中,编译器并不知道$是什么东西,这时我们需要使用declare var来定义它的类型:
declare var $:(select:string)=>any;
$('#foo')上例中,declare var并没有真的定义一个变量,只是定义了全局变量$的类型,仅仅会用域编译时的检查,在编译结果中会被删除。
$('#foo')什么是声明文件
通常我们会把声明语句放到一个单独的文件(jQuery.d.ts)中,这就是声明文件:
// src/jQuery.d.ts
declare var jQuery: (selector: string) => any;// src/index.ts
jQuery('#foo');第三方申明文件
推荐的是使用 @types 统一管理第三方库的声明文件。 @types 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例:
npm install @types/jquery --save-dev可以在这个页面搜索你需要的声明文件。
书写声明文件
当一个第三方库没有提供申明文件时,我们就需要自己书写声明文件了。前面只介绍了嘴贱的声明文件内容,而真正的书写一个声明文件并不是一个简单事情,以下会详细介绍如何书写声明文件。
在不同场景下,声明文件的内容和使用方式会有所区别。
库的使用常见主要有一下集中:
- 全局变量:通过
<script>标签引入的第三方库,注入全局变量。 - npm包:通过
import foo from 'foo'导入,符合es6模块规范 - UMD库:既可以通过
<script>标签映入,又可以通过import导入 - 直接扩展全局变量:通过
<script>标签引入后,改变一个全局变量的结构 - 在npm包或者UMD库中扩展全局变量:引用npm包或者UMD库后,改变一个全局变量的结构
- 模块插件:通过
<script>和import导入后,改变另一个模块的结构
全局变量
全局变量是最简单的一种场景,之间举得例子就是通过<script>标签引入的jQuery,注入的全局变量$和jQuery。 使用全局变量的声明文件时候,如果是以npm install @types/xxx --save-dev安装的,则不需要任何配置。如果是将声明文件直接放于当前项目重,则建议和其他源码一起放到scr目录下(或者对应的源码目录下)。
├── src
| ├── index.ts
| └── jQuery.d.ts
└── tsconfig.json如果没有生效,可以检查下tsconfig.json中的file、include和exclue配置,确保其包含了.d.ts文件。
全局变量的声明文件主要有以下几种语法
declare var声明全局变量declare function声明全局方法declare class声明全局类declare enum声明全局枚举类型declare namespace声明(含有子属性)全局对象interface和type声明全局类型
declare var
在所有的声明语句中,declare var是最简单的,如之前所学,它能够用来定义一个全局的变量的类型。与其类似,还有declare let和declare const,,使用let与使用var没有什么区别:
// src/jquery.d.ts
declare let Query:(selector:string)=>any;
Query('#foo')
// 可以修改
Query = function(select){
return document.querySelector(select)
}而当我们使用const定义时,表示此时的全局变量是一个常量,不允许再去修改他的值了:
declare const Query:(selector:string)=>any;
Query('#foo')
// 不可以修改
Query = function(select){
return document.querySelector(select)
} // 无法分配到 "Query" ,因为它是常数。一般来说,全局变量都是禁止修改的常量,所以大部分情况都应该使用const而不是var和let。 需要注意的是,声明语句中只能定义类型,切勿在声明语句中定义具体的实现:
declare const jQuery = function (select) {
return document.querySelector(select)
}//(local function)(select: any): any 环境上下文中的 "const" 初始化表达式必须为字符串、数字文本或文本枚举引用。declare function
declare function用来定义全局函数的类型。
declare const Query:(selector:string)=>any;在函数类型的声明语句中,函数重载也是支持的:
declare function jQuery(selector: string): any;
declare function jQuery(domReadyCallback: () => any): any;declare class 当全局变量是一类的时候,我们用
declare class来定义他的类型:
declare class Animal {
name:string
constructor(name : string)
sayHi():string
}同样的,declare class语句也只能用来定义类型,不能用来定义具体的实现。
declare enum 使用
declare enum定义的枚举类型也称作外部枚举。
declare enum Directions {
Up,
Down,
Left,
Right
}使用
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];其中,Directions是第三方库定义好的全局变量。 与其他全局变量的类型声明一致,declare enum仅用来定义类型,而不是具体的值。 和正常的类型一样,仅仅用于编译时的检查,声明文件的内容在编译结果中会被删除。
declare namespace
namespace是TypeScript早起为了姐姐模块化而创造的关键字,中文成为命名空间。 由于历史遗留原因,在早起还没有ES6的时候,ts提供了一种模块方案,使用module关键字表示内部模块。但由于后来es6也使用了module关键字,ts为了兼容 es6,使用namespace替代了自己的module,更名为命名空间。 随着es6的广泛应用,现在已经不建议使用ts中的namespace,而推荐使用ES6的模块化方案。namespace被淘汰了,但是在声明文件中,declare namespace还是比较常用的,他用来表示全局变量是一个对象,包含很多子属性。
declare namespace jQuery{
function ajax(url:string,setting?:any):void;
}声明了一个全局变量jQuery,提供一个jQuery.ajax方法可以调用。 在内部我们使用function ajax来声明函数,而不是declare function,类似的,我们也可以使用const、var、let 和enum。
嵌套的命名空间
如果对象拥有深层的层级,则需要用嵌套的namespace来声明深层的属性的类型:
declare namespace jQuery{
function ajax(url:string,setting?:any):void;
namespace fn{
function extend(object:any):void
}
}假如jQuery下仅有fn这一个属性(没有ajax等其他属性或方法),则可以不需要嵌套namespace:
declare namespace jQuery.fn {
function extend(object: any): void;
}interface 和 type
除了全局变量之外,可能还有一些类型我们也希望能暴露出来,在声明文件中,我们可以直接使用interface或type来声明一个全局的接口或者类型:
interface AjaxSettings {
method?:'GET' | 'POST',
data?: any;
}
declare namespace jQuery {
function ajax(url: string, settings?: AjaxSettings): void;
}
```ts
这样话的,在其他的文件也可以使用这个接口或类型了。
##### 防止命名冲突
暴露在最外层的`interface`或`type`会作为全局类型作用于整个项目中,我们应该尽可能的减少全局变量或者全局类型的数量。故最好将他们放在`namespace`中。declare namespace myInterface {
interface a {
name: string
}
}
##### 声明合并
假如jQuery既是一个函数,可以直接被调用,又是一个对象,那么我们可以组合多种声明语句,他们会不冲突的合并起来。declare namespace jQuery {
function ajax(url: string, settings?: AjaxSettings): void;
}
declare namespace jQuery {
}
#### npm 包
一般我们通过(`import foo from 'foo`)导入一个npm包,这是符合ES6模块规范的。
在我们尝试给一个npm包创建声明文件之前,需要先看看它的声明文件是否已经存在,一般来说,npm包的声明文件可能存于两个地方:
1. 与该npm包绑定在一起。判断依据是`package.json`中有`types`字段,或者有一个`index.d.ts`声明文件。这种模式不需要额外安装其他包,是最为退件的,所以以后我们自己创建npm包的时候,最好也将声明文件与`npm`包绑定在一起。
2. 发布到`@types`里。我们只需要尝试安装一下对应的`@types`包就知道是否存在改声明文件`npm install @types/foo --save-dev`。这种模式一般是由于 npm 包的维护者没有提供声明文件,所以只能由其他人将声明文件发布到 @types 里了。
假如上面两种方式都没有找到对应的声明文件,那么我们就需要自己为他写声明文件了。由于是通过`import`语句导入的模块,所以声明文件存放的文职也有所约束,一般有两种方案:
1. 创建一个 node_modules/@types/foo/index.d.ts 文件,存放 foo 模块的声明文件。这种方式不需要额外的配置,但是 node_modules 目录不稳定,代码也没有被保存到仓库中,无法回溯版本,有不小心被删除的风险,故不太建议用这种方案,一般只用作临时测试。
2. 创建一个 types 目录,专门用来管理自己写的声明文件,将 foo 的声明文件放到 types/foo/index.d.ts 中。这种方式需要配置下 tsconfig.json 中的 paths 和 baseUrl 字段。/path/to/project
├── src
| └── index.ts
├── types
| └── foo
| └── index.d.ts
└── tsconfig.json
`tsconfig.json` 内容:{
"compilerOptions": {
"module": "commonjs",
"baseUrl": "./",
"paths": {
"*": ["types/*"]
}
}
}
如此配置之后,通过 import 导入 foo 的时候,也会去 types 目录下寻找对应的模块的声明文件了。
不管采用了以上两种方式中的哪一种,我们都强烈建议大家将书写好的声明文件(通过给第三方发pull request,或者直接提交到@types)发布到开源社区中。
npm 包的声明文件主要有以下几种语法:
- `export`导出变量
- `export namespace` 导出含有(子属性)的对象
- `export = ` commonjs导出模块
> export
npm包的声明文件与全局变量的声明文件有很大区别。在npm包的声明文件中,使用`declare`不再会声明一个全局保留,而只会声明一个局部变量。只有在声明文件中使用`export`导出,然后在使用方`import`导入后,才会应用到这些类型声明。
`export`的语法与普通的ts中的语法类似,区别仅在于声明文件中禁止定义具体的实现。export const name: string;
export function getName(): string;
export class Animal {
constructor(name: string);
sayHi(): string;
}
export enum Directions {
Up,
Down,
Left,
Right
}
export interface Options {
data: any;
}
对应的导入和使用模块应该是这样:import { name, getName, Animal, Directions, Options } from 'foo';
##### 混用`declare`和`export`
我们也可以使用`declare`先声明多个变量,最后再用`export`一次性导出。declare const name: string;
declare function getName(): string;
declare class Animal {
constructor(name: string);
sayHi(): string;
}
declare enum Directions {
Up,
Down,
Left,
Right
}
interface Options {
data: any;
}
export { name, getName, Animal, Directions, Options };
注意,与全局变量的声明文件类似,interface 前是不需要 declare 的。
> export namespace
与`declare namespace`类似,`declare namespace`用来导出一个拥有子属性的对象:export namespace foo {
const name: string;
namespace bar {
function baz(): string;
}
}
> export default
在ES6模块系统中,使用`export default`可以导出一个默认值,使用方可以用 `import foo from ""`而不是`import {foo} from ""`export default function foo(): string;
注意,只有`function`、`class`和`interface`可以直接默认导出,其他的变量需要先定义出来,再默认导出。
> export =
在`commonjs`规范中,我们用一下方式来导出一个模块:// 整体导出 module.exports = foo; // 单个导出 export.bar = bar
在ts中,针对这种模块导出,有多重方式可以导入。
require:// 整体导入 const foo = require('foo') // 单个导入 const bar = require('foo).bar
import:// 整体导入 import * as foo from 'foo'; // 单个导入 import { bar } from 'foo';
import require:// 整体导入 import foo = require('foo'); // 单个导入 import bar = foo.bar;
对于这种使用`commonjs`规范的库,加入要为他写类型声明文件的话,就需要使用到`export=`:export = foo; declare function foo():string; declare namespace foo { const bar:number; }
需要注意的是,上例中使用了`export=`之后,就不能再单个导出`export {bar}`了,所以我们通过声明合并,使用`declare namesapce foo`将`bar`合并到`foo`里。
准确的讲,`export = `不仅可以用在声明文件中,也可以用在普通的ts文件中。实际上,`import ... require` 和 `export = `都是ts为了兼容AMD和commonjs规范创立的新语法,并不常用也不推荐使用。
由于很多第三方库是commonjs规范的,所以声明文件也就不得不使用到这种语法了。但是如果可以,更加退件使用ES6标准的`export default`和`export`。
#### UMD库
即可以通过`<script>`标签引入,有可以通过`import`导入的库,称为UMD库。相比于npm库,我们需要额外声明一个全局变量,为了实现这种方式,ts提供了一个新语法`export as namesapce`。
> export as namespace
一般使用`export as namesace`时,都是现有了npm包的声明文件,再基于它添加一条`export as namespace`语句,即可将声明变成全局变量:
```ts
export as namesace foo;
export = foo;
declare function foo():string
declare namespace foo {
const bat : number
}当然他也可以和export default一起使用:
export as namespace foo;
export default foo;
declare function foo(): string;
declare namespace foo {
const bar: number;
}直接扩展全局变量
有的第三方库扩展了一个全局变量,可是次全局变量的类型缺没有相应的更新过来,就会导致ts编译错误,此时就需要扩展全局变量的类型:
interface String{
prependHello():string
}
'foo'.prependHello()通过声明合并,使用interface+类型,可以给String添加属性或方法。 也可以使用declare namesace给已有的命名空间调加类型声明:
declare namespace JQuery{
interface CusyomOptions{
bar:string
}
}
interface JQueryStatic{
foo(Options:JQuery.CusyomOptions):string
}
jQuery.foo({bar:''})在npm包或者UMD库中扩展全局变量
如之前所说,对于一个npm包或者UMD库的声明文件,只有export导出的类型声明才能被导入。所以对于npm包或者UMD库,如果导入词库之后会扩展全局变量,则需要使用另一语法在声明文件中扩展全局变量的类型,那么declare global。
declare global
使用declare global可以在npm包或者UMD库的声明文件中扩展全局变量的类型:
declare global{
interface String{
prependHello():string
}
}
export {}
'bar'.prependHello();注意 即使此时声明文件不需要导出任何东西,仍需要导出一个空对象,用来告诉编译器这是一个模块的声明文件,而不是一个全局的声明文件。
模块插件
有时候import导入一个模块插件,可以改变另一个模块的结构。此时,如果原有模块以及有了声明文件,而插件模块没有声明文件,就会导致类型不完整,缺少插件部分的类型。ts提供了一个语法declare module,它可以用来扩展原有模块的类型。
declare module
如果是需要扩展原有模块的话,需要在类型声明文件中先引用原有模块,再使用declare module扩展原有模块:
import * as mement form 'mement'
declare.module 'moment'{
export function foo().mement.CalendarKey
}也可以用于在一个文件中,一次性声明多个模块的类型
declare module 'foo'{
export interface Foo {
foo:string
}
}
declare module 'bar' {
export function bar(): string;
}声明文件中的依赖
一个声明文件有时会依赖另一个文件中的类型,比如前面的declare module的例子中,我们就在声明文件中导入了mement,并使用了moment.CalendarKey这个类型:
import * as moment from 'moment';
declare module 'moment' {
export function foo(): moment.CalendarKey;
}除了可以在声明文件中通过import导入另一个声明文件中的类型之外,还有一个语法可以用来导入另一个声明文件,那即是三斜线指令
三斜线指令
与namesapce类似,三斜线指令也是ts在早起版本中为了描述模块之间的依赖关系而穿凿的语法,随着 es6的广泛应用,现在已经不建议在使用ts中的三斜线指令来什么模块之间的依赖关系了。 但是在声明文件中,他还是有一定的用武之地。 类似import,他可以用来导入另一个声明文件。区别在于,当以下几个场景中,我们CIA需要使用三斜线指令代替import:
- 当我们在书写一个全局变量的时候
- 当我需要依赖一个全局变量的声明文件时
书写一个全局变量的声明文件
这些场景听上去很拗口,但是实际上很好理解——————在全局变量的声明文件中,是不允许出现import、export关键字的。一旦出现了,那么他就会视为一个npm包或者UMD库,就不再是全局变量的声明文件了。所以在这种情况下,我们需要引用另一个库的类型,就必须用三斜杠指令了。
// types/jquery-plugin/index.d.ts
/// <reference types="jquery">
declare function foo(Options:JQuery.AjaxSettings):string三斜杠指令的语法后面使用了xml的格式添加了对jquery类型的依赖,这样就可以在声明文件中使用JQuery.AjaxSettings类型了。 注意,三斜杠指令必须放在文件的最顶端,三斜杠指令的前面只允许出现单行或者多行的注释。
依赖一个全局变量的声明文件
在另一个场景下,当我们需要依赖一个全局变量的声明文件时,由于全局变量不支持通过import导入,当然也就必须使用三斜线指令来引入
// types/node-plugin/index.d.ts
/// <reference types="node">
export function foo(p:NodeJS.process):string;上面例子中,由于引入的是node中的全局变量的类型,他们是没有办法用过import来导入的,所以这种场景下也只能通过三斜线指令来引入。 以上两种情况下,只能通过三斜线指令引入。当前模块不支持import和要引入的声明文件不支持import。
拆分声明文件
当我们的全局变量声明文件太大时,可以通过拆分为多个文件,然后在一个入口文件中将他们一一引入,来提高代码的可维护性,比如:
// node_modules/@types/jquery/index.d.ts
/// <reference types="sizzle" />
/// <reference path="JQueryStatic.d.ts" />
/// <reference path="JQuery.d.ts" />
/// <reference path="misc.d.ts" />
/// <reference path="legacy.d.ts" />
export = jQuery;其中用到了types和path两种不同的指令。他们的区别是:types用于声明对另一个库的依赖,而path用于声明对另一个文件的依赖。 上例子中,sizzle和jquery是平行的另一个库,所以使用types来引入,其余的是将jquery拆分成几个不同的文件,通过path引入。
其他三斜线指令
除了这两种三斜线指令之外,还有其他三种指令。
/// <reference no-default-lib="true" />,
/// <amd-module />但它们都是废弃的语法。
自动生成声明文件
如果库的源码就是由ts写的,那么使用tsc脚本将ts编译成js的时候,添加declaration选项就可以同时也生成.d.ts声明文件了。 我们也可以在命令行中添加-- declaration(简写-d),或在tsconfig.json中添加declaration选项。
- declarationDir 设置生成 .d.ts 文件的目录
- declarationMap 对每个 .d.ts 文件,都生成对应的 .d.ts.map(sourcemap)文件
- emitDeclarationOnly 仅生成 .d.ts 文件,不生成 .js 文件
发布声明文件
当我们为一个库写好了声明文件之后,下一步就是将它发布出去,此时有两种方案:
- 将声明文件和源码放在一起。
- 将声明文件发布到
@types下。 这两种方案中有限选择第一种方案,保持声明文件和源码在一起,使用时候就不需要额外增加单独的声明文件库依赖了,而且也能保证声明文件的版本和源码的版本保持一致。 仅当我们再给别人的仓库添加类型声明文件,但是原作者不愿意合并pull request时,CIA需要使用第二种方案,将声明文件发布到@types下
将声明文件和源码放在一起
如果声明文件是通过tsc生成的,那么无须做任何其他配置,只需要吧编译好的文件也发布到npm上,使用方就能获取类型提示了。 如果是手动写的声明文件,那么需要满足一下条件之一,才能被正确的识别:
- 给package.json 中的
types或者typings字段指定一个类型的声明地址 - 在项目根目录下,编写一个
index.d.ts文件 - 针对入口文件(package.json中的
main字段指定的入口文件),编写一个同名不同后缀的.d.ts文件
第一种方式:
{
"name": "foo",
"version": "1.0.0",
"main": "lib/index.js",
"types": "foo.d.ts",
}指定了types为foo.d.ts之后,导入此库的时候,就会去找foo.d.ts作为此库的类型声明文件了。 typings与types一样,只是另一种写法。 如果没有指定types或typings,那么就会在根目录下寻找index.d.ts文件,将它视为此库的类型声明文件。 如果没有找到index.d.ts文件,那么就会寻找入口文件中的main文件指定的入口文件是否存在对应同名不用后缀的.d.ts文件。 类似这样:
{
"name": "foo",
"version": "1.0.0",
"main": "lib/index.js"
}package.json中不含有types和typings字符安,就会寻找是否存在index.d.ts文件。如果还不存在,那么就会寻找是否存在lib/index.d.ts文件。假如还是不存在,就会认为没有一个没有提供类型声明文件的库。
有的库为了支持导入子模块,比如import bar form 'foo/lib/bar' ,就需要额外在编写一个类型声明文件lib/bar.d.ts或者lib/bar/index.d.ts,这与自动生成声明文件类似,一个库中同时包含了多个类型声明文件。
将声明文件发布到@types下
如果我们是给别人的仓库添加类型声明文件,但原作者不愿意合并pull request,那么就需要将声明文件发布到@types下。 与普通的npm模块不同,@types是统一由DefinitelyTyped管理。要将声明文件发布到@types下,就穿件一个pr,其中包含类型声明文件,测试代码,以及tsconfig.json等。 pull-request 需要符合它们的规范,并且通过测试,才能被合并,稍后就会被自动发布到 @types 下。
在 DefinitelyTyped 中创建一个新的类型声明,需要用到一些工具,DefinitelyTyped 的文档中已经有了详细的介绍,这里就不赘述了,以官方文档为准。
如果大家有此类需求,可以参考下笔者提交的 pull-request 。
