TypeScript 声明文件

xx.d.ts

Posted by huangqing on April 10, 2021

声明语法

  • declare var 声明全局变量
  • declare function 声明全局方法
  • declare class 声明全局类
  • declare enum 声明全局枚举类型
  • declare namespace 声明(含有子属性的)全局对象
  • interfacetype 声明全局类型
  • export 导出变量
  • export namespace 导出(含有子属性的)对象
  • export default ES6 默认导出
  • export = commonjs 导出模块
  • export as namespace UMD 库声明全局变量
  • declare global 扩展全局变量
  • declare module 扩展模块
  • /// <reference /> 三斜线指令

声明文件

通常我们会把声明语句放到一个单独的文件(jQuery.d.ts)中,这就是声明文件. 声明只能用来定义类型,不能用来定义具体的实现.

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

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

ts 会解析项目中所有的 *.ts 文件,当然也包含以 .d.ts 结尾的文件。所以当我们将 jQuery.d.ts 放到项目中时,其他所有 *.ts 文件就都可以获得 jQuery 的类型定义了

/path/to/project
├── src
|  ├── index.ts
|  └── jQuery.d.ts
└── tsconfig.json

假如仍然无法解析,那么可以检查下 tsconfig.json 中的 filesincludeexclude 配置,确保其包含了 jQuery.d.ts 文件。

第三方声明文件

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

npm install @types/jquery --save-dev

使用typescriptlang search页面搜索你需要的声明文件

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

// src/jQuery.d.ts
// 全局变量这种模式的声明文件
declare var jQuery: (selector: string) => any;

库的使用场景

当一个第三方库没有提供声明文件时,我们就需要自己书写声明文件.

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

  • 全局变量:通过 <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

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

  • declare var 声明全局变量
  • declare function 声明全局方法
  • declare class 声明全局类
  • declare enum 声明全局枚举类型
  • declare namespace 声明(含有子属性的)全局对象
  • interfacetype 声明全局类型

declare var

在所有的声明语句中,declare var 是最简单的,如之前所学,它能够用来定义一个全局变量的类型。与其类似的,还有 declare letdeclare const,使用 let 与使用 var 没有什么区别:

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

一般来说,全局变量都是禁止修改的常量,所以大部分情况都应该使用 const 而不是 varlet

declare function

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

// src/jQuery.d.ts
declare function jQuery(selector: string): any;

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

// src/jQuery.d.ts
declare function jQuery(selector: string): any;
declare function jQuery(domReadyCallback: () => any): any;

declare class

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

// src/Animal.d.ts
declare class Animal {
    name: string;
    constructor(name: string);
    sayHi(): string;
}

declare enum

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

// src/Directions.d.ts
declare enum Directions {
    Up,
    Down,
    Left,
    Right
}

与其他全局变量的类型声明一致,declare enum 仅用来定义类型,而不是具体的值。

declare namespace

namespace 被淘汰了,但是在声明文件中,declare namespace 还是比较常用的,它用来表示全局变量是一个对象,包含很多子属性。

// src/jQuery.d.ts
declare namespace jQuery {
    function ajax(url: string, settings?: any): void;
    const version: number;
    class Event {
        blur(eventType: EventType): void
    }
    enum EventType {
        CustomClick
    }
}

嵌套的命名空间

// src/jQuery.d.ts
declare namespace jQuery {
    function ajax(url: string, settings?: any): void;
    namespace fn {
        function extend(object: any): void;
    }
}

interfacetype

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

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

防止命名冲突

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

// src/jQuery.d.ts

declare namespace jQuery {
    interface AjaxSettings {
        method?: 'GET' | 'POST'
        data?: any;
    }
    function ajax(url: string, settings?: AjaxSettings): void;
}

声明合并

假如 jQuery 既是一个函数,可以直接被调用 jQuery('#foo'),又是一个对象,拥有子属性 jQuery.ajax(),那么我们可以组合多个声明语句,它们会不冲突的合并起来

// src/jQuery.d.ts

declare function jQuery(selector: string): any;
declare namespace jQuery {
    function ajax(url: string, settings?: any): void;
}

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 里了。

假如以上两种方式都没有找到对应的声明文件,那么我们就需要自己为它写声明文件了。

创建一个 types 目录,专门用来管理自己写的声明文件,将 foo 的声明文件放到 types/foo/index.d.ts 中。这种方式需要配置下 tsconfig.json 中的 pathsbaseUrl 字段。

/path/to/project
├── src
|  └── index.ts
├── types
|  └── foo
|     └── index.d.ts
└── tsconfig.json

tsconfig.json 内容:

{
  "compilerOptions": {
    "module": "commonjs",
    "baseUrl": "./",
    "paths": {
      "*": ["types/*"]
    }
  }
}

npm 包的声明:

  • export 导出变量
  • export namespace 导出(含有子属性的)对象
  • export default ES6 默认导出
  • export = commonjs 导出模块

export

npm 包的声明文件与全局变量的声明文件有很大区别。在 npm 包的声明文件中,使用 declare 不再会声明一个全局变量,而只会在当前文件中声明一个局部变量。只有在声明文件中使用 export 导出,然后在使用方 import 导入后,才会应用到这些类型声明。

只要 .ts.d.ts 文件中有 importexport,那么这个文件中的 declare 就会变成局部变量

export 的语法与普通的 ts 中的语法类似,区别仅在于声明文件中禁止定义具体的实现:

// types/foo/index.d.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;
}
// src/index.ts

import { 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",
  },
};

export namespace

declare namespace 类似,export namespace 用来导出一个拥有子属性的对象:

// types/foo/index.d.ts

export namespace foo {
    const name: string;
    namespace bar {
        function baz(): string;
    }
}
// src/index.ts

import { 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 用来导出默认值的类型:

// types/foo/index.d.ts
export default function foo(): string;
// src/index.ts
import foo from "foo";
foo();

注意,只有 functionclassinterface 可以直接默认导出,其他的变量需要先定义出来,再默认导出.

export =

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

// 整体导出
module.exports = foo;
// 单个导出
exports.bar = bar;

导入模块:

import ... require

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

UMD 库

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

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

// types/foo/index.d.ts

export as namespace foo;
export = foo;
//export default foo;

declare function foo(): string;
declare namespace foo {
    const bar: number;
}

扩展全局变量

直接扩展全局变量

有的第三方库扩展了一个全局变量,可是此全局变量的类型却没有相应的更新过来,就会导致 ts 编译错误,此时就需要扩展全局变量的类型。比如扩展 String 类型:

interface String {
  prependHello(): string;
}

"foo".prependHello();

通过声明合并,使用 interface String 即可给 String 添加属性或方法。

也可以使用 declare namespace 给已有的命名空间添加类型声明:

// types/jquery-plugin/index.d.ts

declare namespace JQuery {
    interface CustomOptions {
        bar: string;
    }
}

interface JQueryStatic {
    foo(options: JQuery.CustomOptions): string;
}
// src/index.ts

jQuery.foo({
  bar: "",
});

在 npm 包或 UMD 库中扩展全局变量

如之前所说,对于一个 npm 包或者 UMD 库的声明文件,只有 export 导出的类型声明才能被导入。所以对于 npm 包或 UMD 库,如果导入此库之后会扩展全局变量,则需要使用另一种语法在声明文件中扩展全局变量的类型,那就是 declare global

declare global

使用 declare global 可以在 npm 包或者 UMD 库的声明文件中扩展全局变量的类型:

// types/foo/index.d.ts
declare global {
  interface String {
  prependHello(): string;
  }
}

export {};

注意即使此声明文件不需要导出任何东西,仍然需要导出一个空对象,用来告诉编译器这是一个模块的声明文件,而不是一个全局变量的声明文件。

// src/index.ts
"bar".prependHello();

模块插件§

有时通过 import 导入一个模块插件,可以改变另一个原有模块的结构。此时如果原有模块已经有了类型声明文件,而插件模块没有类型声明文件,就会导致类型不完整,缺少插件部分的类型。ts 提供了一个语法 declare module,它可以用来扩展原有模块的类型。

declare module

如果是需要扩展原有模块的话,需要在类型声明文件中先引用原有模块,再使用 declare module 扩展原有模块:

// types/moment-plugin/index.d.ts

import * as moment from 'moment';

declare module 'moment' {
    export function foo(): moment.CalendarKey;
}
// src/index.ts

import * as moment from "moment";
import "moment-plugin";

moment.foo();

自动生成声明文件§

如果库的源码本身就是由 ts 写的,那么在使用 tsc 脚本将 ts 编译为 js 的时候,添加 declaration 选项,就可以同时也生成 .d.ts 声明文件了。

我们可以在命令行中添加 --declaration(简写 -d),或者在 tsconfig.json 中添加 declaration 选项。这里以 tsconfig.json 为例:

{
  "compilerOptions": {
    "module": "commonjs",
    "outDir": "lib",
    "declaration": true
  }
}

发布声明文件

当我们为一个库写好了声明文件之后,下一步就是将它发布出去了。

此时有两种方案:

  • 将声明文件和源码放在一起
  • 将声明文件发布到 @types

这两种方案中优先选择第一种方案。保持声明文件与源码在一起,使用时就不需要额外增加单独的声明文件库的依赖了,而且也能保证声明文件的版本与源码的版本保持一致。

仅当我们在给别人的仓库添加类型声明文件,但原作者不愿意合并 pull request 时,才需要使用第二种方案,将声明文件发布到 @types 下。

将声明文件和源码放在一起

如果声明文件是通过 tsc 自动生成的,那么无需做任何其他配置,只需要把编译好的文件也发布到 npm 上,使用方就可以获取到类型提示了。

如果是手动写的声明文件,那么需要满足以下条件之一,才能被正确的识别:

  • package.json 中的 typestypings 字段指定一个类型声明文件地址
  • 在项目根目录下,编写一个 index.d.ts 文件
  • 针对入口文件(package.json 中的 main 字段指定的入口文件),编写一个同名不同后缀的 .d.ts 文件

第一种方式是给 package.json 中的 typestypings 字段指定一个类型声明文件地址。比如:

{
  "name": "foo",
  "version": "1.0.0",
  "main": "lib/index.js",
  "types": "foo.d.ts"
}

指定了 typesfoo.d.ts 之后,导入此库的时候,就会去找 foo.d.ts 作为此库的类型声明文件了。

typingstypes 一样,只是另一种写法。

如果没有指定 typestypings,那么就会在根目录下寻找 index.d.ts 文件,将它视为此库的类型声明文件。

如果没有找到 index.d.ts 文件,那么就会寻找入口文件(package.json 中的 main 字段指定的入口文件)是否存在对应同名不同后缀的 .d.ts 文件。

将声明文件发布到 @types

如果我们是在给别人的仓库添加类型声明文件,但原作者不愿意合并 pull request,那么就需要将声明文件发布到 @types 下。

与普通的 npm 模块不同,@types 是统一由 DefinitelyTyped 管理的。要将声明文件发布到 @types 下,就需要给 DefinitelyTyped 创建一个 pull-request,其中包含了类型声明文件,测试代码,以及 tsconfig.json 等。

pull-request 需要符合它们的规范,并且通过测试,才能被合并,稍后就会被自动发布到 @types 下。

在 DefinitelyTyped 中创建一个新的类型声明,需要用到一些工具,DefinitelyTyped 的文档中已经有了详细的介绍.

教程