TS07 声明文件

TypeScript中声明文件。

声明语句

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

比如使用jQuery时,我们获取一个元素:

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

但是这时编译器并不知道$或者jQuery是什么,所以我们需要使用declare定义它的类型

1
2
3
declare var jQuery: (selector: string) => any;

jQuery('#foo');

上面的declare var就是声明语句,它并没有定义一个变量,只是定义了jQuery的类型,仅仅用于编译时的检查,编译结果中会删除。

声明文件

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

1
2
3
// src/jQuery.d.ts

declare var jQuery: (selector: string) => any;

声明文件必须以.d.ts为后缀,当我们将jQuery.d.ts放入项目中,其他所有的*.ts文件就都可以获得jQuery的类型定义了

如果无法解析,可以检查一下tsconfig.json中的filesincludeexclude配置,确保包含了声明文件

第三方声明文件

大部分热门的第三方库的声明文件都不需要我们自己定义了,我们可以直接使用@types统一管理第三方库的声明文件。

可以在这个页面搜索需要的声明文件,然后使用npm安装对应的声明模块即可:

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

书写全局变量

如果第三方库没有提供声明文件,我们需要自己书写声明了,在不同的场景下,声明文件的内容和使用方式有所区别

当通过<script>标签引入第三方库的时候,会注入全局变量,就像上面的例子一样。建议将声明文件和源码一起放到scr目录下。

注意,声明全局变量时,文件中不能包括export关键字,否则就会报错。

声明变量

可以使用declare vardeclare letdeclare const来声明变量,一般来说全局变量都是禁止修改的常量,所以大部分情况都应该使用const

1
declare const jQuery: (selector: string) => any;

要注意的是,声明语句中只能定义类型,不要在声明语句中定义具体的实现

声明函数

使用declare function来声明全局函数的类型,jQuery其实就是一个函数,所以也可以使用function来定义。在函数类型的声明语句中,也支持函数重载

1
2
declare function jQuery(cb: () => any)
declare function jQuery(selector: string): any;

声明类

使用declare class定义一个类:

1
2
3
4
5
6
declare class Person {
name: string;
constructor(name: string) ;
sayHi(): string;
sayBye: (msg: string) => string
}

同样的,declare class也只能定义类型,不能定义具体实现

声明枚举类型

JS中是没有枚举类型的,TypeScript中使用declare enum声明的枚举类型也成为外部枚举,定义后的变量不能包含枚举值之外的属性

1
2
3
4
5
6
7
8
9
10
11
declare enum Direction {
up,
down,
left,
right,
}

const d1 = Direction.down;

const d2 = Direction.south;
// Error:(17, 22) TS2339: Property 'south' does not exist on type 'typeof Direction'.

声明命名空间

使用declare namespace声明命名空间,用来表示全局变量是一个对象,包含很多子属性。

比如jQuery是一个全局变量,它是一个对象,提供了一个jQuery.ajax的方法可以调用,那么我们就可以通过declare namespace来声明这个拥有很多个子属性的全局变量:

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

declare namespace内部,直接使用function来声明函数,而不是使用declare function,类似的也可以使用classenumconst等语句

1
2
3
4
5
6
7
8
9
10
11
12
13
declare namespace jQuery {
function ajax(url: string, settings?: any): void;

const version: number;

class Event {
blur(eventType: EventType): void
}

enum EventType {
CustomClick
}
}

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

1
2
3
4
5
6
7
declare namespace jQuery {
function ajax(url: string, settings?: any): void;

namespace fn {
function extend(object: any): void;
}
}

假如jQuery下仅有fn这一个属性(没有ajax等其他属性或方法),则可以不需要嵌套namespace

1
2
3
declare namespace jQuery.fn {
function extend(object: any): void;
}

声明全局的接口或类型

我们可以将接口和其他的类型放到类型声明文件中,这样声明的接口或者类型就暴露成为全局的接口或类型,可以被其他文件使用

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

要注意,暴露在全局的interface或者type会作为全局类型作用域整个项目中,存在命名冲突的可能性,所以应该尽量减少全局变量或全局类型的数量,所以应该将他们放到namespace下:

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

使用这个interface的时候,应该加上命名空间的前缀:

1
2
3
4
5
6
let settings: jQuery.AjaxSettings = {
method: 'POST',
data: {
name: 'foo'
}
};

声明合并

如果一个对象即是一个函数,可以直接调用jquery('#foo'),又是一个对象,有子属性jQuery.ajax(),那么可以组合多了个声明语句,他们会不冲突的合并:

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

合并规则后面单独学习。

为NPM包创建声明文件

找到已存在的声明文件

在给引入的NPM包创建声明文件之前,先看它的声明文件是否存在,一般来说可能存在于:

(1)与包绑定在一起,看package.jsontypes字段,或者看包中是否有index.d.ts声明文件。

推荐这种模式,因为不需要安装额外的其他包。我们自己创建NPM包的时候,最好也将声明与包绑定在一起

(2)发布到@types里,可以去上面提到的页面搜索对应的声明文件,然后安装即可。

这种模式一般是由第三方提供的声明文件,发布到@types中。

编写声明文件

如果没有找到声明文件,我们可以自己编写。由于一般是通过import来引入一个NPM包(假设为foo),所以声明文件的存放位置有要求。

最常用的方案是,创建一个types目录,专门用来管理自己写的声明文件,将自己编写的声明文件放到types/foo/index.d.ts中。

这种方式还需要配置tsconfig.json中的pathsbaseUrl字段。

1
2
3
4
5
6
7
8
9
{
"compilerOptions": {
"module": "commonjs",
"baseUrl": "./",
"paths": {
"*": ["types/*"]
}
}
}

NPM声明文件包含下面几种语法:

export导出变量

在NPM包的声明文件中,如果使用declare不会再声明全局变量,只会在当前文件中声明局部变量。局部变量需要使用export导出,然后由使用方import导入后,才会被应用。

同样,声明文件中不能定义具体的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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;
}

也可以使用declare先声明多个变量,然后再用export一次性导出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// types/foo/index.d.ts

declare 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};

export namesapce

declare namspace一样,export namespace导出一个拥有子属性的对象

1
2
3
4
5
6
7
8
// types/foo/index.d.ts

export namespace foo {
const name: string;
namespace bar {
function baz(): string;
}
}

可以使用export default导出默认的functionclassinterface,这三者可以直接导出,其他的类型需要先定义,然后在使用export default导出,一般会将这种导出放在整个声明文件的最前面:

1
2
3
4
5
6
7
8
9
10
// types/foo/index.d.ts

export default Directions;

declare enum Directions {
Up,
Down,
Left,
Right
}

UMD

针对UMD格式的模块,使用export as namespace进行导出,一般使用时,都是先有了NPM包的声明文件,再基于它添加export as namespace语句,就可以将声明好的一个变量声明为全局变量:

1
2
3
4
5
6
7
8
9
// types/foo/index.d.ts

export as namespace foo;
export = foo;

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

扩展全局变量的类型

可以使用declare global来在已有的声明文件中扩展全局变量的类型:

1
2
3
4
5
6
7
8
9
// types/foo/index.d.ts

declare global {
interface String {
prependHello(): string;
}
}

export {};

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

可以使用declare module来扩展模块插件

自动生成声明文件

如果库的源码本身就是TypeScript编写的,那么使用tsc来将.ts编译为JS的过程中,可以添加declaration选项(简写-d,同时生成.d.ts声明文件

也可以在ts.config中添加declaration选项来实现:

1
2
3
4
5
6
7
{
"compilerOptions": {
"module": "commonjs",
"outDir": "lib",
"declaration": true,
}
}

这样就会由.ts文件生成.d.ts声明文件,并且输出到lib目录下:

这样做的时候,每个.ts文件都会对应一个.d.ts声明文件,这样使用方就可以再使用import导入时获得类型提示

此外,tsconfig.json中还有其他选项与自动生成声明文件相关:

  • declarationDir,设置生成.d.ts文件的目录
  • declarationsMap,对每个.d.ts文件都生成对应的.d.ts.map(sourcemap)文件
  • emitDeclarationOnly,仅仅生成.d.ts文件,不生成.js文件

发布声明文件

如果是tsc命令自动生成的声明文件,不需要做任何其他配置,直接发布到NPM即可

如果是手动编写的,需要满足下面条件之一,才能被正确识别:

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

参考