声明文件:
当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
新语法索引
由于本章涉及大量新语法,故在本章开头列出新语法的索引,方便大家在使用这些新语法时能快速查找到对应的讲解:
-
declare var声明全局变量 -
declare function声明全局方法 -
declare class声明全局类 -
declare enum声明全局枚举类型 -
declare namespace声明(含有子属性的)全局对象 -
interface和type声明全局类型 -
export导出变量 -
export namespace导出(含有子属性的)对象 -
export defaultES6 默认导出 -
export =commonjs 导出模块 -
export as namespaceUMD 库声明全局变量 -
declare global扩展全局变量 -
declare module扩展模块 -
/// <reference />三斜线指令什么是声明语句
假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过
<script>标签引入 jQuery,然后就可以使用全局变量$或jQuery了。我们通常这样获取一个
id是foo的元素:$('#foo');// orjQuery('#foo');但是在 ts 中,编译器并不知道
$或jQuery是什么东西1:jQuery('#foo');// ERROR: Cannot find name 'jQuery'.这时,我们需要使用
declare var来定义它的类型2:declare var jQuery: (selector: string) => any;jQuery('#foo');上例中,
declare var并没有真的定义一个变量,只是定义了全局变量jQuery的类型,仅仅会用于编译时的检查,在编译结果中会被删除。它编译结果是:jQuery('#foo');除了
declare var之外,还有其他很多种声明语句。什么是声明文件:通常我们会把声明语句放到一个单独的文件(
jQuery.d.ts)中,这就是声明文件3:// src/jQuery.d.tsdeclare var jQuery: (selector: string) => any;// src/index.tsjQuery('#foo');声明文件必需以
.d.ts为后缀。一般来说,ts 会解析项目中所有的
*.ts文件,当然也包含以.d.ts结尾的文件。所以当我们将jQuery.d.ts放到项目中时,其他所有*.ts文件就都可以获得jQuery的类型定义了。/path/to/project├── src| ├── index.ts| └── jQuery.d.ts└── tsconfig.json假如仍然无法解析,那么可以检查下
tsconfig.json中的files、include和exclude配置,确保其包含了jQuery.d.ts文件。这里只演示了全局变量这种模式的声明文件,假如是通过模块导入的方式使用第三方库的话,那么引入声明文件又是另一种方式了。
第三方声明文件当然,jQuery 的声明文件不需要我们定义了,社区已经帮我们定义好了:jQuery in DefinitelyTyped。
我们可以直接下载下来使用,但是更推荐的是使用
@types统一管理第三方库的声明文件。@types的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例:npm install @types/jquery --save-dev可以在 https://microsoft.github.io/TypeSearch/搜索你需要的声明文件。书写声明文件:当一个第三方库没有提供声明文件时,我们就需要自己书写声明文件了。前面只介绍了最简单的声明文件内容,而真正书写一个声明文件并不是一件简单的事,以下会详细介绍如何书写声明文件。
在不同的场景下,声明文件的内容和使用方式会有所区别。
库的使用场景主要有以下几种:
-
全局变量:通过
<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安装的,则不需要任何配置。如果是将声明文件直接存放于当前项目中,则建议和其他源码一起放到src目录下(或者对应的源码目录下):/path/to/project├── src| ├── index.ts| └── jQuery.d.ts└── tsconfig.json如果没有生效,可以检查下
tsconfig.json中的files、include和exclude配置,确保其包含了jQuery.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.tsdeclare let jQuery: (selector: string) => any;// src/index.tsjQuery('#foo');// 使用 declare let 定义的 jQuery 类型,允许修改这个全局变量jQuery = function(selector) {return document.querySelector(selector);};而当我们使用const定义时,表示此时的全局变量是一个常量,不允许再去修改它的值了4:// src/jQuery.d.tsdeclare const jQuery: (selector: string) => any;jQuery('#foo');// 使用 declare const 定义的 jQuery 类型,禁止修改这个全局变量jQuery = function(selector) {return document.querySelector(selector);};// ERROR: Cannot assign to 'jQuery' because it is a constant or a read-only property.一般来说,全局变量都是禁止修改的常量,所以大部分情况都应该使用
const而不是var或let。需要注意的是,声明语句中只能定义类型,切勿在声明语句中定义具体的实现5:
declare const jQuery = function(selector) {return document.querySelector(selector);};// ERROR: An implementation cannot be declared in ambient contexts.declare function:declare function用来定义全局函数的类型。jQuery 其实就是一个函数,所以也可以用function来定义:// src/jQuery.d.tsdeclare function jQuery(selector: string): any;// src/index.tsjQuery('#foo');
-
在函数类型的声明语句中,函数重载也是支持的6:// src/jQuery.d.tsdeclare function jQuery(selector: string): any;declare function jQuery(domReadyCallback: () => any): any;// src/index.tsjQuery('#foo');jQuery(function() {alert('Dom Ready!');});declare class:当全局变量是一个类的时候,我们用declare class来定义它的类型7:// src/Animal.d.tsdeclare class Animal {name: string;constructor(name: string);sayHi(): string;}// src/index.tslet cat = new Animal('Tom');同样的,
declare class语句也只能用来定义类型,不能用来定义具体的实现,比如定义sayHi方法的具体实现则会报错:// src/Animal.d.tsdeclare class Animal {name: string;constructor(name: string);sayHi() {return `My name is ${this.name}`;};// ERROR: An implementation cannot be declared in ambient contexts.}declare enum:
使用
declare enum定义的枚举类型也称作外部枚举(Ambient Enums),举例如下8:// src/Directions.d.tsdeclare enum Directions {Up,Down,Left,Right}// src/index.tslet directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];与其他全局变量的类型声明一致,
declare enum仅用来定义类型,而不是具体的值。Directions.d.ts仅仅会用于编译时的检查,声明文件里的内容在编译结果中会被删除。它编译结果是:var directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];其中
Directions是由第三方库定义好的全局变量。declare namespacenamespace是 ts 早期时为了解决模块化而创造的关键字,中文称为命名空间。由于历史遗留原因,在早期还没有 ES6 的时候,ts 提供了一种模块化方案,使用
module关键字表示内部模块。但由于后来 ES6 也使用了module关键字,ts 为了兼容 ES6,使用namespace替代了自己的module,更名为命名空间。随着 ES6 的广泛应用,现在已经不建议再使用 ts 中的
namespace,而推荐使用 ES6 的模块化方案了,故我们不再需要学习namespace的使用了。namespace被淘汰了,但是在声明文件中,declare namespace还是比较常用的,它用来表示全局变量是一个对象,包含很多子属性。比如
jQuery是一个全局变量,它是一个对象,提供了一个jQuery.ajax方法可以调用,那么我们就应该使用declare namespace jQuery来声明这个拥有多个子属性的全局变量。// src/jQuery.d.tsdeclare namespace jQuery {function ajax(url: string, settings?: any): void;}// src/index.tsjQuery.ajax('/api/get_something');注意,在
declare namespace内部,我们直接使用function ajax来声明函数,而不是使用declare function ajax。类似的,也可以使用const,class,enum等语句9:// src/jQuery.d.tsdeclare namespace jQuery {function ajax(url: string, settings?: any): void;const version: number;class Event {blur(eventType: EventType): void}enum EventType {CustomClick}}// src/index.tsjQuery.ajax('/api/get_something');console.log(jQuery.version);const e = new jQuery.Event();e.blur(jQuery.EventType.CustomClick);嵌套的命名空间:如果对象拥有深层的层级,则需要用嵌套的namespace来声明深层的属性的类型10:// src/jQuery.d.tsdeclare namespace jQuery {function ajax(url: string, settings?: any): void;namespace fn {function extend(object: any): void;}}// src/index.tsjQuery.ajax('/api/get_something');jQuery.fn.extend({check: function() {return this.each(function() {this.checked = true;});}});假如jQuery下仅有fn这一个属性(没有ajax等其他属性或方法),则可以不需要嵌套namespace11:// src/jQuery.d.tsdeclare namespace jQuery.fn {function extend(object: any): void;}// src/index.tsjQuery.fn.extend({check: function() {return this.each(function() {this.checked = true;});}});interface和type:除了全局变量之外,可能有一些类型我们也希望能暴露出来。在类型声明文件中,我们可以直接使用
interface或type来声明一个全局的接口或类型12:// src/jQuery.d.tsinterface AjaxSettings {method?: 'GET' | 'POST'data?: any;}declare namespace jQuery {function ajax(url: string, settings?: AjaxSettings): void;}这样的话,在其他文件中也可以使用这个接口或类型了:
// src/index.tslet settings: AjaxSettings = {method: 'POST',data: {name: 'foo'}};jQuery.ajax('/api/post_something', settings);type与interface类似,不再赘述。防止命名冲突:
暴露在最外层的
interface或type会作为全局类型作用于整个项目中,我们应该尽可能的减少全局变量或全局类型的数量。故最好将他们放到namespace下13:// src/jQuery.d.tsdeclare namespace jQuery {interface AjaxSettings {method?: 'GET' | 'POST'data?: any;}function ajax(url: string, settings?: AjaxSettings): void;}注意,在使用这个
interface的时候,也应该加上jQuery前缀:// src/index.tslet settings: jQuery.AjaxSettings = {method: 'POST',data: {name: 'foo'}};jQuery.ajax('/api/post_something', settings);声明合并:假如 jQuery 既是一个函数,可以直接被调用
jQuery('#foo'),又是一个对象,拥有子属性jQuery.ajax()(事实确实如此),那么我们可以组合多个声明语句,它们会不冲突的合并起来14:// src/jQuery.d.tsdeclare function jQuery(selector: string): any;declare namespace jQuery {function ajax(url: string, settings?: any): void;}// src/index.tsjQuery('#foo');jQuery.ajax('/api/get_something');----------------------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.jsontsconfig.json内容:{"compilerOptions": {"module": "commonjs","baseUrl": "./","paths": {"*": ["types/*"]}}}如此配置之后,通过import导入foo的时候,也会去types目录下寻找对应的模块的声明文件了。注意
module配置可以有很多种选项,不同的选项会影响模块的导入导出模式。这里我们使用了commonjs这个最常用的选项,后面的教程也都默认使用的这个选项。不管采用了以上两种方式中的哪一种,
npm 包的声明文件主要有以下几种语法:
-
export导出变量 -
export namespace导出(含有子属性的)对象 -
export defaultES6 默认导出
export =commonjs 导出模块export
npm 包的声明文件与全局变量的声明文件有很大区别。在 npm 包的声明文件中,使用
declare不再会声明一个全局变量,而只会在当前文件中声明一个局部变量。只有在声明文件中使用export导出,然后在使用方import导入后,才会应用到这些类型声明。export的语法与普通的 ts 中的语法类似,区别仅在于声明文件中禁止定义具体的实现15:// types/foo/index.d.tsexport 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;}对应的导入和使用模块应该是这样:// src/index.tsimport { name, getName, Animal, Directions, Options } from 'foo';console.log(name);let myName = getName();let cat = new Animal('Tom');let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];let options: Options = {data: {name: 'foo'}};混用 declare 和 export我们也可以使用declare先声明多个变量,最后再用export一次性导出。上例的声明文件可以等价的改写为16:// types/foo/index.d.tsdeclare 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类似,export namespace用来导出一个拥有子属性的对象17:// types/foo/index.d.tsexport namespace foo {const name: string;namespace bar {function baz(): string;}}// src/index.tsimport { foo } from 'foo';console.log(foo.name);foo.bar.baz();export default:在 ES6 模块系统中,使用
export default可以导出一个默认值,使用方可以用import foo from 'foo'而不是import { foo } from 'foo'来导入这个默认值。在类型声明文件中,
export default用来导出默认值的类型18:// types/foo/index.d.tsexport default function foo(): string;// src/index.tsimport foo from 'foo';foo();注意,只有function、class和interface可以直接默认导出,其他的变量需要先定义出来,再默认导出19:// types/foo/index.d.tsexport default enum Directions {// ERROR: Expression expected.Up,Down,Left,Right}上例中
export default enum是错误的语法,需要使用declare enum定义出来,然后使用export default导出:// types/foo/index.d.tsdeclare enum Directions {Up,Down,Left,Right}export default Directions;针对这种默认导出,我们一般会将导出语句放在整个声明文件的最前面20:
// types/foo/index.d.tsexport default Directions;declare enum Directions {Up,Down,Left,Right}export =在 commonjs 规范中,我们用以下方式来导出一个模块:
// 整体导出module.exports = foo;// 单个导出exports.bar = bar;在 ts 中,针对这种模块导出,有多种方式可以导入,第一种方式是
const ... = require:// 整体导入const foo = require('foo');// 单个导入const bar = require('foo').bar;第二种方式是
import ... from,注意针对整体导出,需要使用import * as来导入:// 整体导入import * as foo from 'foo';// 单个导入import { bar } from 'foo';第三种方式是
import ... require,这也是 ts 官方推荐的方式:// 整体导入import foo = require('foo');// 单个导入import bar = foo.bar;对于这种使用 commonjs 规范的库,假如要为它写类型声明文件的话,就需要使用到
export =这种语法了21:// types/foo/index.d.tsexport = foo;declare function foo(): string;declare namespace foo {const bar: number;}需要注意的是,上例中使用了
export =之后,就不能再单个导出export { bar }了。所以我们通过声明合并,使用declare namespace foo来将bar合并到foo里。准确地讲,
export =不仅可以用在声明文件中,也可以用在普通的 ts 文件中。实际上,import ... require和export =都是 ts 为了兼容 AMD 规范和 commonjs 规范而创立的新语法,由于并不常用也不推荐使用,所以这里就不详细介绍了,感兴趣的可以看官方文档。由于很多第三方库是 commonjs 规范的,所以声明文件也就不得不用到
export =这种语法了。但是还是需要再强调下,相比与export =,我们更推荐使用 ES6 标准的export default和export。UMD 库
既可以通过
<script>标签引入,又可以通过import导入的库,称为 UMD 库。相比于 npm 包的类型声明文件,我们需要额外声明一个全局变量,为了实现这种方式,ts 提供了一个新语法export as namespace。export as namespace一般使用
export as namespace时,都是先有了 npm 包的声明文件,再基于它添加一条export as namespace语句,即可将声明好的一个变量声明为全局变量,举例如下22:// types/foo/index.d.tsexport as namespace foo;export = foo;declare function foo(): string;declare namespace foo {const bar: number;}当然它也可以与
export default一起使用:// types/foo/index.d.tsexport as namespace foo;export default foo;declare function foo(): string;declare namespace foo {const bar: number;}直接扩展全局变量:
有的第三方库扩展了一个全局变量,可是此全局变量的类型却没有相应的更新过来,就会导致 ts 编译错误,此时就需要扩展全局变量的类型。比如扩展
String类型23:interface String {prependHello(): string;}'foo'.prependHello();通过声明合并,使用
interface String即可给String添加属性或方法。也可以使用
declare namespace给已有的命名空间添加类型声明24:// types/jquery-plugin/index.d.tsdeclare namespace JQuery {interface CustomOptions {bar: string;}}interface JQueryStatic {foo(options: JQuery.CustomOptions): string;}// src/index.tsjQuery.foo({bar: ''});在 npm 包或 UMD 库中扩展全局变量如之前所说,对于一个 npm 包或者 UMD 库的声明文件,只有export导出的类型声明才能被导入。所以对于 npm 包或 UMD 库,如果导入此库之后会扩展全局变量,则需要使用另一种语法在声明文件中扩展全局变量的类型,那就是declare global。declare global使用
declare global可以在 npm 包或者 UMD 库的声明文件中扩展全局变量的类型25:// types/foo/index.d.tsdeclare global {interface String {prependHello(): string;}}export {};// src/index.ts'bar'.prependHello();注意即使此声明文件不需要导出任何东西,仍然需要导出一个空对象,用来告诉编译器这是一个模块的声明文件,而不是一个全局变量的声明文件。
模块插件:
有时通过
import导入一个模块插件,可以改变另一个原有模块的结构。此时如果原有模块已经有了类型声明文件,而插件模块没有类型声明文件,就会导致类型不完整,缺少插件部分的类型。ts 提供了一个语法
declare module,它可以用来扩展原有模块的类型。declare module
如果是需要扩展原有模块的话,需要在类型声明文件中先引用原有模块,再使用
declare module扩展原有模块26:// types/moment-plugin/index.d.tsimport * as moment from 'moment';declare module 'moment' {export function foo(): moment.CalendarKey;}// src/index.tsimport * as moment from 'moment';import 'moment-plugin';moment.foo();declare module也可用于在一个文件中一次性声明多个模块的类型27:// types/foo-bar.d.tsdeclare module 'foo' {export interface Foo {foo: string;}}declare module 'bar' {export function bar(): string;}// src/index.tsimport { Foo } from 'foo';import * as bar from 'bar';let f: Foo;bar.bar();声明文件中的依赖一个声明文件有时会依赖另一个声明文件中的类型,比如在前面的
declare module的例子中,我们就在声明文件中导入了moment,并且使用了moment.CalendarKey这个类型:// types/moment-plugin/index.d.tsimport * as moment from 'moment';declare module 'moment' {export function foo(): moment.CalendarKey;}除了可以在声明文件中通过
import导入另一个声明文件中的类型之外,还有一个语法也可以用来导入另一个声明文件,那就是三斜线指令。三斜线指令与
namespace类似,三斜线指令也是 ts 在早期版本中为了描述模块之间的依赖关系而创造的语法。随着 ES6 的广泛应用,现在已经不建议再使用 ts 中的三斜线指令来声明模块之间的依赖关系了。但是在声明文件中,它还是有一定的用武之地。
类似于声明文件中的
import,它可以用来导入另一个声明文件。与import的区别是,当且仅当在以下几个场景下,我们才需要使用三斜线指令替代import:-
当我们在书写一个全局变量的声明文件时
-
当我们需要依赖一个全局变量的声明文件时
书写一个全局变量的声明文件
这些场景听上去很拗口,但实际上很好理解——在全局变量的声明文件中,是不允许出现
import,export关键字的。一旦出现了,那么他就会被视为一个 npm 包或 UMD 库,就不再是全局变量的声明文件了。故当我们在书写一个全局变量的声明文件时,如果需要引用另一个库的类型,那么就必须用三斜线指令了28:// types/jquery-plugin/index.d.ts/// <reference types="jquery" />declare function foo(options: JQuery.AjaxSettings): string;// src/index.tsfoo({});三斜线指令的语法如上,
///后面使用 xml 的格式添加了对jquery类型的依赖,这样就可以在声明文件中使用JQuery.AjaxSettings类型了。注意,三斜线指令必须放在文件的最顶端,三斜线指令的前面只允许出现单行或多行注释。
依赖一个全局变量的声明文件
在另一个场景下,当我们需要依赖一个全局变量的声明文件时,由于全局变量不支持通过
import导入,当然也就必须使用三斜线指令来引入了29:// types/node-plugin/index.d.ts/// <reference types="node" />export function foo(p: NodeJS.Process): string;// src/index.tsimport { foo } from 'node-plugin';foo(global.process);在上面的例子中,我们通过三斜线指引入了
node的类型,然后在声明文件中使用了NodeJS.Process这个类型。最后在使用到foo的时候,传入了node中的全局变量process。由于引入的
node中的类型都是全局变量的类型,它们是没有办法通过import来导入的,所以这种场景下也只能通过三斜线指令来引入了。以上两种使用场景下,都是由于需要书写或需要依赖全局变量的声明文件,所以必须使用三斜线指令。在其他的一些不是必要使用三斜线指令的情况下,就都需要使用
import来导入。拆分声明文件
当我们的全局变量的声明文件太大时,可以通过拆分为多个文件,然后在一个入口文件中将它们一一引入,来提高代码的可维护性。比如
jQuery的声明文件就是这样的:// 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="sizzle"来声明对它的依赖。而其他的三斜线指令就是将jquery的声明拆分到不同的文件中了,然后在这个入口文件中使用path="foo"将它们一一引入。其他三斜线指令
除了这两种三斜线指令之外,还有其他的三斜线指令,比如
/// <reference no-default-lib="true"/>,/// <amd-module />等,但它们都是废弃的语法,故这里就不介绍了.自动生成声明文件:如果库的源码本身就是由 ts 写的,那么在使用
tsc脚本将 ts 编译为 js 的时候,添加declaration选项,就可以同时也生成.d.ts声明文件了。我们可以在命令行中添加
--declaration(简写-d),或者在tsconfig.json中添加declaration选项。这里以tsconfig.json为例:{"compilerOptions": {"module": "commonjs","outDir": "lib","declaration": true,}}上例中我们添加了
outDir选项,将 ts 文件的编译结果输出到lib目录下,然后添加了declaration选项,设置为true,表示将会由 ts 文件自动生成.d.ts声明文件,也会输出到lib目录下。运行
tsc之后,目录结构如下30:/path/to/project├── lib| ├── bar| | ├── index.d.ts| | └── index.js| ├── index.d.ts| └── index.js├── src| ├── bar| | └── index.ts| └── index.ts├── package.json└── tsconfig.json在这个例子中,
src目录下有两个 ts 文件,分别是src/index.ts和src/bar/index.ts,它们被编译到lib目录下的同时,也会生成对应的两个声明文件lib/index.d.ts和lib/bar/index.d.ts。它们的内容分别是:// src/index.tsexport * from './bar';export default function foo() {return 'foo';}// src/bar/index.tsexport function bar() {return 'bar';}// lib/index.d.tsexport * from './bar';export default function foo(): string;// lib/bar/index.d.tsexport declare function bar(): string;可见,自动生成的声明文件基本保持了源码的结构,而将具体实现去掉了,生成了对应的类型声明。
使用
tsc自动生成声明文件时,每个 ts 文件都会对应一个.d.ts声明文件。这样的好处是,使用方不仅可以在使用import foo from 'foo'导入默认的模块时获得类型提示,还可以在使用import bar from 'foo/lib/bar'导入一个子模块时,也获得对应的类型提示。除了
declaration选项之外,还有几个选项也与自动生成声明文件有关,这里只简单列举出来,不做详细演示了:-
declarationDir设置生成.d.ts文件的目录 -
declarationMap对每个.d.ts文件,都生成对应的.d.ts.map(sourcemap)文件 -
emitDeclarationOnly仅生成.d.ts文件,不生成.js文件
当我们为一个库写好了声明文件之后,下一步就是将它发布出去了。
此时有两种方案:
-
将声明文件和源码放在一起
-
将声明文件发布到
@types下
这两种方案中优先选择第一种方案。保持声明文件与源码在一起,使用时就不需要额外增加单独的声明文件库的依赖了,而且也能保证声明文件的版本与源码的版本保持一致。
仅当我们在给别人的仓库添加类型声明文件,但原作者不愿意合并 pull request 时,才需要使用第二种方案,将声明文件发布到
@types下。将声明文件和源码放在一起:
如果声明文件是通过
tsc自动生成的,那么无需做任何其他配置,只需要把编译好的文件也发布到 npm 上,使用方就可以获取到类型提示了。如果是手动写的声明文件,那么需要满足以下条件之一,才能被正确的识别:
-
给
package.json中的types或typings字段指定一个类型声明文件地址 -
在项目根目录下,编写一个
index.d.ts文件 -
针对入口文件(
package.json中的main字段指定的入口文件),编写一个同名不同后缀的.d.ts文件
第一种方式是给
package.json中的types或typings字段指定一个类型声明文件地址。比如:{"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文件,那么就会寻找入口文件(package.json中的main字段指定的入口文件)是否存在对应同名不同后缀的.d.ts文件。比如
package.json是这样时:{"name": "foo","version": "1.0.0","main": "lib/index.js"}就会先识别
package.json中是否存在types或typings字段。发现不存在,那么就会寻找是否存在index.d.ts文件。如果还是不存在,那么就会寻找是否存在lib/index.d.ts文件。假如说连lib/index.d.ts都不存在的话,就会被认为是一个没有提供类型声明文件的库了。有的库为了支持导入子模块,比如
import bar from 'foo/lib/bar',就需要额外再编写一个类型声明文件lib/bar.d.ts或者lib/bar/index.d.ts,这与自动生成声明文件类似,一个库中同时包含了多个类型声明文件。将声明文件发布到@types下如果我们是在给别人的仓库添加类型声明文件,但原作者不愿意合并 pull request,那么就需要将声明文件发布到
@types下。与普通的 npm 模块不同,
@types是统一由 DefinitelyTyped 管理的。要将声明文件发布到@types下,就需要给 DefinitelyTyped 创建一个 pull-request,其中包含了类型声明文件,测试代码,以及tsconfig.json等。pull-request 需要符合它们的规范,并且通过测试,才能被合并,稍后就会被自动发布到
@types下。在 DefinitelyTyped 中创建一个新的类型声明,需要用到一些工具,DefinitelyTyped 的文档中已经有了详细的介绍,这里就不赘述了,以官方文档为准。
-