当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。

新语法索引

什么是声明语句

假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过 <script> 标签引入 jQuery,然后就可以使用全局变量 $jQuery 了。
但是在 ts 中,编译器并不知道 $jQuery 是什么东西

  1. jQuery('#foo');
  2. // ERROR: Cannot find name 'jQuery'.

这时,我们需要使用 declare var 来定义它的类型

  1. declare var jQuery: (selector: string) => any;
  2. jQuery('#foo');

上例中,declare var 并没有真的定义一个变量,只是定义了全局变量 jQuery 的类型,仅仅会用于编译时的检查,在编译结果中会被删除。它编译结果是:

  1. jQuery('#foo');

除了 declare var 之外,还有其他很多种声明语句,将会在后面详细介绍。

什么是声明文件

通常我们会把声明语句放到一个单独的文件(jQuery.d.ts)中,这就是声明文件

  1. // src/jQuery.d.ts
  2. declare var jQuery: (selector: string) => any;
  1. // src/index.ts
  2. jQuery('#foo');

声明文件必需以 .d.ts 为后缀。

第三方声明文件

我们可以直接下载下来使用,但是更推荐的是使用 @types 统一管理第三方库的声明文件。
@types 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例:

  1. npm install @types/jquery --save-dev

可以在这个页面搜索你需要的声明文件。(https://www.typescriptlang.org/dt/search?search=)

书写声明文件

当一个第三方库没有提供声明文件时,我们就需要自己书写声明文件了。前面只介绍了最简单的声明文件内容,而真正书写一个声明文件并不是一件简单的事,以下会详细介绍如何书写声明文件。
在不同的场景下,声明文件的内容和使用方式会有所区别。
库的使用场景主要有以下几种:

全局变量

全局变量是最简单的一种场景,之前举的例子就是通过 <script> 标签引入 jQuery,注入全局变量 $jQuery。如果没有生效,查看教程:
https://ts.xcatliu.com/basics/declaration-files.html#npm-bao

全局变量的声明文件主要有以下几种语法:

全局变量声明类中:

  1. // src/jQuery.d.ts
  2. declare let jQuery: (selector: string) => any;

类的使用中:

  1. // src/index.ts
  2. jQuery('#foo');
  3. // 使用 declare let 定义的 jQuery 类型,允许修改这个全局变量
  4. jQuery = function(selector) {
  5. return document.querySelector(selector);
  6. };

而当我们使用 const 定义时,表示此时的全局变量是一个常量,不允许再去修改它的值了
一般来说,全局变量都是禁止修改的常量,所以大部分情况都应该使用 const 而不是 varlet
需要注意的是,声明语句中只能定义类型,切勿在声明语句中定义具体的实现

  1. declare const jQuery = function(selector) {
  2. return document.querySelector(selector);
  3. };
  4. // ERROR: An implementation cannot be declared in ambient contexts.

declare function

declare function 用来定义全局函数的类型。jQuery 其实就是一个函数,所以也可以用 function 来定义:

  1. // src/jQuery.d.ts
  2. declare function jQuery(selector: string): any;
  1. // src/index.ts
  2. jQuery('#foo');

在函数类型的声明语句中,函数重载也是支持的

  1. // src/jQuery.d.ts
  2. declare function jQuery(selector: string): any;
  3. declare function jQuery(domReadyCallback: () => any): any;
  1. // src/index.ts
  2. jQuery('#foo');
  3. jQuery(function() {
  4. alert('Dom Ready!');
  5. });

declare class

当全局变量是一个类的时候,我们用 declare class 来定义它的类型

  1. // src/Animal.d.ts
  2. declare class Animal {
  3. name: string;
  4. constructor(name: string);
  5. sayHi(): string;
  6. }
  1. // src/index.ts
  2. let cat = new Animal('Tom');

同样的,declare class 语句也只能用来定义类型,不能用来定义具体的实现,比如定义 sayHi 方法的具体实现则会报错:

  1. // src/Animal.d.ts
  2. declare class Animal {
  3. name: string;
  4. constructor(name: string);
  5. sayHi() {
  6. return `My name is ${this.name}`;
  7. };
  8. // ERROR: An implementation cannot be declared in ambient contexts.
  9. }

declare enum§

使用 declare enum 定义的枚举类型也称作外部枚举(Ambient Enums),举例如下

  1. // src/Directions.d.ts
  2. declare enum Directions {
  3. Up,
  4. Down,
  5. Left,
  6. Right
  7. }
  1. // src/index.ts
  2. let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

与其他全局变量的类型声明一致,declare enum 仅用来定义类型,而不是具体的值。
Directions.d.ts 仅仅会用于编译时的检查,声明文件里的内容在编译结果中会被删除。它编译结果是:

  1. var directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

其中 Directions 是由第三方库定义好的全局变量。

declare namespace§

namespace 是 ts 早期时为了解决模块化而创造的关键字,中文称为命名空间。
namespace 被淘汰了,但是在声明文件中,declare namespace 还是比较常用的,它用来表示全局变量是一个对象,包含很多子属性。

嵌套的命名空间

如果对象拥有深层的层级,则需要用嵌套的 namespace 来声明深层的属性的类型

  1. // src/jQuery.d.ts
  2. declare namespace jQuery {
  3. function ajax(url: string, settings?: any): void;
  4. namespace fn {
  5. function extend(object: any): void;
  6. }
  7. }
  1. // src/index.ts
  2. jQuery.ajax('/api/get_something');
  3. jQuery.fn.extend({
  4. check: function() {
  5. return this.each(function() {
  6. this.checked = true;
  7. });
  8. }
  9. });

interfacetype

除了全局变量之外,可能有一些类型我们也希望能暴露出来。在类型声明文件中,我们可以直接使用 interfacetype 来声明一个全局的接口或类型

  1. // src/jQuery.d.ts
  2. interface AjaxSettings {
  3. method?: 'GET' | 'POST'
  4. data?: any;
  5. }
  6. declare namespace jQuery {
  7. function ajax(url: string, settings?: AjaxSettings): void;
  8. }

这样的话,在其他文件中也可以使用这个接口或类型了:

  1. // src/index.ts
  2. let settings: AjaxSettings = {
  3. method: 'POST',
  4. data: {
  5. name: 'foo'
  6. }
  7. };
  8. jQuery.ajax('/api/post_something', settings);

typeinterface 类似,不再赘述。

防止命名冲突

暴露在最外层的 interfacetype 会作为全局类型作用于整个项目中,我们应该尽可能的减少全局变量或全局类型的数量。故最好将他们放到 namespace

  1. // src/jQuery.d.ts
  2. declare namespace jQuery {
  3. interface AjaxSettings {
  4. method?: 'GET' | 'POST'
  5. data?: any;
  6. }
  7. function ajax(url: string, settings?: AjaxSettings): void;
  8. }

注意,在使用这个 interface 的时候,也应该加上 jQuery 前缀:

  1. // src/index.ts
  2. let settings: jQuery.AjaxSettings = {
  3. method: 'POST',
  4. data: {
  5. name: 'foo'
  6. }
  7. };
  8. jQuery.ajax('/api/post_something', settings);

npm 包

npm 包的声明文件主要有以下几种语法:

在 commonjs 规范中,我们用以下方式来导出一个模块:

  1. // 整体导出
  2. module.exports = foo;
  3. // 单个导出
  4. exports.bar = bar;

在 ts 中,针对这种模块导出,有多种方式可以导入,第一种方式是 const ... = require

  1. // 整体导入
  2. const foo = require('foo');
  3. // 单个导入
  4. const bar = require('foo').bar;

第二种方式是 import ... from,注意针对整体导出,需要使用 import * as 来导入:

  1. // 整体导入
  2. import * as foo from 'foo';
  3. // 单个导入
  4. import { bar } from 'foo';

第三种方式是 import ... require,这也是 ts 官方推荐的方式:

  1. // 整体导入
  2. import foo = require('foo');
  3. // 单个导入
  4. import bar = foo.bar;

UMD 库

既可以通过 <script> 标签引入,又可以通过 import 导入的库,称为 UMD 库。

export as namespace

一般使用 export as namespace 时,都是先有了 npm 包的声明文件,再基于它添加一条 export as namespace 语句,即可将声明好的一个变量声明为全局变量