新语法索引

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

什么是声明文件

.d.ts 结尾。一般来说,ts 会解析项目中所有的 *.ts 文件,当然也包含以 .d.ts 结尾的文件。所以当我们将某个 x.d.ts 文件放到项目中时,其他所有 *.ts 文件就都可以获得它的类型定义了。手动书写声明文件有以下几种场景

  • 通常直接放到项目内部的(是指除 types 文件夹)情况有两种
  • 全局声明文件,通过前面列出来的声明全局类型的语法,对全局的变量的类型进行定义
  • 对 js 文件进行类型定义,通过在同一目录下,声明同名不同后缀(.d.ts)的文件,通过前面列出来的几种 export 方式,可以对该 .js 文件的导出成员类型进行定义
  • 放到项目内的 types 文件夹,通常是对第三方模块的类型进行补充,例如某些没有类型声明的第三方库,或者自己需要对第三方库扩展类型等,文件名需要跟模块名同名,tsConfig 的配置如下:
{
    "compilerOptions": {
    ....
        "baseUrl": "./",
        "paths": {
            "*": ["types/*"]
        }
        .....
    }
}

第三方声明文件

对于某些本身没有提供类型声明的第三方库,社区已经帮我们定义好了,我们可以直接下载下来使用,但是更推荐的是使用 @types 统一管理第三方库的声明文件。@types 的使用方式很简单,直接用 npm 安装对应的声明模块即可

书写声明文件

当一个第三方库没有提供声明文件时,我们就需要自己书写声明文件了。前面只介绍了最简单的声明文件内容,而真正书写一个声明文件并不是一件简单的事,以下会详细介绍如何书写声明文件。

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

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

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

全局变量

使用全局变量的声明文件时,如果是以 npm install @types/xxx --save-dev 安装的,则不需要任何配置。如果是将声明文件直接存放于当前项目中,则建议和其他源码一起放到 src 目录下(或者对应的源码目录下)。

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

  • declare var|let|const 声明全局变量
  • declare function 声明全局方法
  • declare class 声明全局类。
  • 当全局变量是一个类的时候使用
  • declare enum 声明全局枚举类型
  • declare namespace 声明(含有子属性的)全局对象
  • namespace 是 ts 早期时为了解决模块化而创造的关键字,中文称为命名空间。由于历史遗留原因,在早期还没有 ES6 的时候,ts 提供了一种模块化方案,使用 module 关键字表示内部模块。但由于后来 ES6 也使用了 module 关键字,ts 为了兼容 ES6,使用 namespace 替代了自己的 module,更名为命名空间。随着 ES6 的广泛应用,现在已经不建议再使用 ts 中的 namespace,而推荐使用 ES6 的模块化方案了,故我们不再需要学习 namespace 的使用了。
  • namespace 被淘汰了,但是在声明文件中,declare namespace 还是比较常用的,它用来表示全局变量是一个对象,包含很多子属性。
  • 如果对象拥有深层的层级,则需要用嵌套的 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
    }
    // 嵌套
    namespace fn {
        function extend(object: any): void;
    }
}

// src/index.ts

jQuery.ajax('/api/get_something');
console.log(jQuery.version);
const e = new jQuery.Event();
e.blur(jQuery.EventType.CustomClick);
jQuery.fn.extend({})
  • interface 和 type 声明全局类型
  • 这两个不需要使用 delcare 关键字就可以直接在声明文件中声明全局类型,但需要注意命名冲突,这里声明的是全局类型

需要注意的是,declare 语句也只能用来定义类型,不能用来定义具体的实现

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 语句导入的模块,所以声明文件存放的位置也有所约束,方案:

  • 创建一个 types 目录,专门用来管理自己写的声明文件,将 foo 的声明文件放到 types/foo/index.d.ts 或者 types.foo.d.ts中。这种方式需要配置下 tsconfig.json 中的 pathsbaseUrl 字段。
// tsConfig.json 内容
{
    "compilerOptions": {
        "baseUrl": "./",
        "paths": {
            "*": ["types/*"]
        }
    }
}

如此配置之后,通过 import 导入 foo 的时候,也会去 types 目录下寻找对应的模块的声明文件了。

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

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

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

// types/foo/index.d.ts

export const name: string;

// src/index.ts
import { name } from 'foo';
混用 declare 和 export

我们也可以使用 declare 先声明多个变量,最后再用 export 一次性导出。上例的声明文件可以等价的改写为:

// types/foo/index.d.ts
declare const name: string;

export { name }

// src/index.ts
import { name } from 'foo';

注意,与全局变量的声明文件类似,interface 前是不需要 declare 的。

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

类似 export,但是注意,只有 function、class 和 interface 可以直接默认导出,其他的变量需要先定义出来,再默认导出

export =

对于某些第三方库,导出时使用的是 commonjs 的方式进行导出。假如要为它写类型声明文件的话,就需要使用到 export = 这种语法了。

// types/foo/index.d.ts

export = foo;

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

UMD 库

既可以通过

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

// types/foo/index.d.ts
export = foo;

declare function foo(): string;
declare namespace foo {
    const bar: number;
}
// 前面是 npm 包的类型声明,基于此再添加下面这句
export as namespace foo;

三斜杠指令

与 namespace 类似,三斜线指令也是 ts 在早期版本中为了描述模块之间的依赖关系而创造的语法。随着 ES6 的广泛应用,现在已经不建议再使用 ts 中的三斜线指令来声明模块之间的依赖关系了。

但是在声明文件中,它还是有一定的用武之地。

类似于声明文件中的 import,它可以用来导入另一个声明文件。与 import 的区别是,当且仅当在以下几个场景下,我们才需要使用三斜线指令替代 import:

  • 当我们在书写一个全局变量的声明文件时
  • 当我们需要依赖一个全局变量的声明文件时
书写一个全局变量的声明文件

这些场景听上去很拗口,但实际上很好理解——在全局变量的声明文件中,是不允许出现 import, export 关键字的。一旦出现了,那么他就会被视为一个 npm 包或 UMD 库,就不再是全局变量的声明文件了。故当我们在书写一个全局变量的声明文件时,如果需要引用另一个库的类型,那么就必须用三斜线指令了

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

/// <reference types="jquery" />

declare function foo(options: JQuery.AjaxSettings): string;
依赖一个全局变量的声明文件

在另一个场景下,当我们需要依赖一个全局变量的声明文件时,由于全局变量不支持通过 import 导入,当然也就必须使用三斜线指令来引入了

// types/node-plugin/index.d.ts
/// <reference types="node" />
export function foo(p: NodeJS.Process): string;

// src/index.ts
import { foo } from 'node-plugin';
foo(global.process);
拆分声明文件

当我们的全局变量的声明文件太大时,可以通过拆分为多个文件,然后在一个入口文件中将它们一一引入,来提高代码的可维护性。比如 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;

其中用到了 typespath 两种不同的指令。它们的区别是:types 用于声明对另一个库的依赖,而 path 用于声明对另一个文件的依赖。

其他三斜线指令

除了这两种三斜线指令之外,还有其他的三斜线指令,比如 /// <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 目录下。例如:

typescript类型声明是全局的吗 typescript 全局声明文件_jQuery


可见,自动生成的声明文件基本保持了源码的结构,而将具体实现去掉了,生成了对应的类型声明。

使用 tsc 自动生成声明文件时,每个 ts 文件都会对应一个 .d.ts 声明文件。这样的好处是,使用方不仅可以在使用 import foo from 'foo' 导入默认的模块时获得类型提示,还可以在使用 import bar from 'foo/lib/bar' 导入一个子模块时,也获得对应的类型提示。

发布声明文件

此时有两种方案:

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

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

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

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

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

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

在查找第三方库的类型声明时,顺序就是这列出来的,优先级从上到下依次递减