当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
新语法索引
declare var
声明全局变量declare function
申明全局函数declare class
声明全局类declare enum
声明全局枚举类型declare namespace
声明(含有子属性的)全局对象interface
和type
声明全局类型export
导出变量export namespace
导出(含有子属性的)对象export default
ES5默认导出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;
}
这样话的,在其他的文件也可以使用这个接口或类型了。
防止命名冲突
暴露在最外层的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包的声明文件可能存于两个地方:
- 与该npm包绑定在一起。判断依据是
package.json
中有types
字段,或者有一个index.d.ts
声明文件。这种模式不需要额外安装其他包,是最为退件的,所以以后我们自己创建npm包的时候,最好也将声明文件与npm
包绑定在一起。 - 发布到
@types
里。我们只需要尝试安装一下对应的@types
包就知道是否存在改声明文件npm install @types/foo --save-dev
。这种模式一般是由于 npm 包的维护者没有提供声明文件,所以只能由其他人将声明文件发布到 @types 里了。
假如上面两种方式都没有找到对应的声明文件,那么我们就需要自己为他写声明文件了。由于是通过import
语句导入的模块,所以声明文件存放的文职也有所约束,一般有两种方案:
- 创建一个 node_modules/@types/foo/index.d.ts 文件,存放 foo 模块的声明文件。这种方式不需要额外的配置,但是 node_modules 目录不稳定,代码也没有被保存到仓库中,无法回溯版本,有不小心被删除的风险,故不太建议用这种方案,一般只用作临时测试。
- 创建一个 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 namesace 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
语句,即可将声明变成全局变量:
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 。