feng xiaohan

声明文件

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

背景

  • 声明文件是形如xx.d.ts的文件类型;

  • 有些第三方库需要并没有编写好声明文件,需要自己去获取;如果是流行的库,可能网上有别人(社区)写好的声明文件,可以尝试下载,声明文件一般为@type/库名;如果是不活跃的库,可能需要我们手动去声明;

    例如:为 express 手写一个声明库(见示例)

书写声明文件

在不同的场景下,声明文件的内容和使用方式会有所区别。

库的使用场景主要有以下几种:

  • 全局变量:通过 <script> 标签引入第三方库,注入全局变量
  • npm 包:通过 import foo from 'foo' 导入,符合 ES6 模块规范
  • UMD 库:既可以通过 <script> 标签引入,又可以通过 import 导入
  • 直接扩展全局变量:通过 <script> 标签引入后,改变一个全局变量的结构
  • 在 npm 包或 UMD 库中扩展全局变量:引用 npm 包或 UMD 库后,改变一个全局变量的结构
  • 模块插件:通过 <script>import 导入后,改变另一个模块的结构

全局变量

注意:所有声明语句中只能定义类型,切勿在声明语句中定义具体的实现

  • declare var

    用来定义一个全局变量的类型

    // src/jQuery.d.ts
    
    declare let jQuery: (selector: string) => any;
    // src/index.ts
    
    jQuery("#foo");
    // 使用 declare let 定义的 jQuery 类型,允许修改这个全局变量
    jQuery = function (selector) {
      return document.querySelector(selector);
    };
    
  • declare function

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

    // src/jQuery.d.ts
    declare function jQuery(selector: string): any;
    
    // src/index.ts
    jQuery("#foo");
    
  • declare class

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

    // src/Animal.d.ts
    
    declare class Animal {
      name: string;
      constructor(name: string);
      sayHi(): string;
    }
    // src/index.ts
    
    let cat = new Animal("Tom");
    
  • declare enum

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

    // src/Directions.d.ts
    
    declare enum Directions {
      Up,
      Down,
      Left,
      Right,
    }
    // src/index.ts
    
    let directions = [
      Directions.Up,
      Directions.Down,
      Directions.Left,
      Directions.Right,
    ];
    
  • declare namespace(淘汰)

  • 嵌套的命名空间(*)

  • interface 和 type

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

    // src/jQuery.d.ts
    
    interface AjaxSettings {
      method?: "GET" | "POST";
      data?: any;
    }
    declare namespace jQuery {
      function ajax(url: string, settings?: AjaxSettings): void;
    }
    

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

    // src/index.ts
    
    let settings: AjaxSettings = {
      method: "POST",
      data: {
        name: "foo",
      },
    };
    jQuery.ajax("/api/post_something", settings);
    

    typeinterface 类似。

  • 防止命名冲突

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

  • 声明合并

UMD 库

既可以通过 <script> 标签引入,又可以通过 import 导入的库,称为 UMD 库。相比于 npm 包的类型声明文件,我们需要额外声明一个全局变量,为了实现这种方式,ts 提供了一个新语法 export as namespace

示例

  • 为 express 手写一个声明库

    // index.ts
    import express from "express";
    
    const app = express();
    const router = express.Router();
    
    app.use("/api", router);
    router.get("/api", (req: any, res: any) => {
      res.json({
        code: 200,
      });
    });
    app.listen(9002, () => {
      console.log("9002");
    });
    
    // express.d.ts
    declare module "express" {
      interface Router {
        get(path: string, cb: (req: any, res: any) => void): void;
      }
      interface App {
        use(path: string, router: any): void;
        listen(port: number, cb?: () => void);
      }
      interface Express {
        (): App;
        Router(): Router;
      }
      const express: Express;
    
      export default express;
    }