TypeScript 在开发的过程中不可避免要引用一些第三方的 JavaScript 库,虽然可以直接调用库里面的所有类和方法,但是使用过程中是缺少了类型检查、代码补全这些功能的,这时候就需要引用声明文件。
就现在使用的 VSCode 工具来说,写 JavaScript 代码时候的代码补全、接口提示这些功能,一部分是通过 VSCode 使用了正则匹配,更多的是依靠 d.ts
声明文件。
声明文件语法
-
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 />
三斜线指令
第三方声明文件
声明文件一般不是和库文件一起发布的,npm
在安装完库文件后是没有声明文件的。但是常用的库文件在 VSCode
中已经安装有了,所以很多时候是不需要自己安装第三方声明文件的。
更推荐使用 @types
统一管理第三方库的声明文件:TypeSearch、npm
也可以使用 npm
来安装 @types
声明文件:
$ npm i @types/jquery --save-dev
书写声明文件
全局变量
全局变量主要是使用了 declare
关键字:
-
declare var
: 声明全局变量 -
declare function
: 声明全局方法 -
declare class
声明全局类 -
declare enum
声明全局枚举类型 -
declare namespace
声明(含有子属性的)全局对象 -
interface
和type
声明全局类型
declare var
declare var
是最简单的声明语句,就是简单地定义一个全局变量的类型。还有 declare let
和 declare const
,和使用 let
、const
没什么区别:
// 声明文件只能定义类型,不能定义具体的实现
declare let jQuery: (selector: string) => any;
// ==========================================
jQuery('#foo');
// let 定义的变量可以被修改
jQuery = function(selector) {return document.querySelector(selector);
}
使用 declare let
定义的变量是可以被修改的,使用 declare const
就没有这样的问题。
一般引用库文件是不会去修改库文件里的变量的,修改全局变量会导致很多问题出现,所以大部分情况下都是用 declare const
而不是 declare let
、declare var
。
declare function
declare
用于定义全局函数的类型,其实 JQuery 就是一个函数,也可以使用 declare function
来定义:
// 下面定义其实和上面差不多,上面的例子是定义了一个变量,这里定义了一个函数
declare function jQuery(selector: string): any;
// ===========================================
jQuery('#foo');
在声明文件里,函数也是支持重载的:
declare function jQuery(selector: string): any;
declare function jQuery(domReadyCallback () => any): any;
// =====================================================
jQuery('#foo');
jQuery(function() {alert('Dom Ready!');
});
declare class
declare class
可以定义一个全局的类:
declare class Animal {
name: string;
constructor(name: string);
sayHi(): string;
sayHello() {
// ERROR: An implementation cannot be declared in ambient contexts.
return 'Hello!';
}
}
// ==========================
const cat = new Animal('Tom);
declare enum
使用 declare enum
定义的枚举类也被称为外部枚举 (Ambient Enums):
declare enum Directions {
Up,
Down,
Left,
Right
}
// ===========
const directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
其实声明文件内的枚举没有多大用处,仅仅编译的时候用于检查,声明文件的内容编译后是会被删除的。
declare namespace
namespace
即命名空间,其实这个算是一个被淘汰的关键字。
在 ES6 还没有出现的时候,TypeScript 使用了 module
关键字表示内部模块,但是后来 ES6 也使用了 module
关键字,TypeScript 为了兼容 ES6 而又使用 namespace
替代 module
。
在 TypeScript 中 namespace
已经不推荐使用了,但是声明文件中还是很常见,declare namespace
表示全局变量是一个对象,包含很多子属性。
比如 jQuery
是一个全局变量,它是一个对象,提供了一个 jQuery.ajax
方法可以调用:
declare namespace jQuery {function ajax(url: string, settings?: any): void;
}
// =================================================
jQuery.ajax('http://www.baidu.com');
在 declare namespace
内部,不用再使用 declare
关键字,可以直接使用 function
、const
、class
、enum
直接声明:
declare namespace jQuery {function ajax(url: string, settings?: any): void;
const version: number;
class Event {blur(eventType: EventType): void
}
enum EventType {CustomClick}
}
namespace
嵌套
对于复杂的数据类型,对象里面包含对象是很常见的,这种时候可以使用嵌套的 namespace
来声明深层的属性类型:
declare namespace jQuery {function ajax(url: string, settings?: any): void;
namespace fn {function extend(object: any): void;
}
}
// =================================================
jQuery.fn.extend({check: function() {return this.each(function() {this.checked = true;});
}
});
在 jQuery
仅有 fn
这一个属性(没有 ajax
这个属性)的时候,也可以不使用嵌套:
declare namespace jQuery.fn {function extend(object: any): void;
}
interface
和 type
declare
这个关键字可以用来定义全局的变量、方法、类、枚举、对象等,想要暴露出全局的接口、类型,不需要 declare
这个关键字,可以直接使用 interface
或 type
来声明一个全局的接口或类型:
interface AjaxSettings {
method?: 'GET' | 'POST'
data?: any;
}
declare namespace jQuery {function ajax(url: string, settings?: AjaxSettings): void;
}
// =======================
const settings: AjaxSettings = {
method: 'POST',
data: {name: 'foo'}
};
jQuery.ajax('http://www.baidu.com', settings);
暴露在最外层的 interface
和 type
是一个全局类型,全局类型的变量当然是越少越好,最好是将 interface
和 type
放到 namespace
下:
declare namespace jQuery {
interface AjaxSettings {
method?: 'GET' | 'POST'
data?: any;
}
function ajax(url: string, settings?: AjaxSettings): void;
}
// ==========================================================
// 使用 interface 时也需要加上 jQuery 前缀
const settings: jQuery.AjaxSettings = {
method: 'POST',
data: {name: 'foo'}
};
jQuery.ajax('/api/post_something', settings);
声明合并
假如 jQuery
既是一个函数,可以直接被调用 jQuery(‘#foo’),又是一个对象,拥有子属性 jQuery.ajax()(事实确实如此),那么我们可以组合多个声明语句,它们会不冲突地合并起来:
declare function jQuery(selector: string): any;
declare namespace jQuery {function ajax(url: string, settings?: any): void;
}
// =================================================
jQuery('#foo');
jQuery.ajax('/api/get_something');
导出变量
一个 npm 包的声明文件可能存在于两个地方:
- 与 npm 包绑定在一起。判断依据是
package.json
中有types
字段,或者有一个index.d.ts
声明文件。 - 发布在
@types
里。尝试安装一下对应的@types
包就知道是否存在该声明文件。
手动写声明文件,一般有两种方案:
- 在
node_modules
目录下加入@types
文件夹,在指定包名文件夹下写index.d.ts
声明文件。这个方案有一个风险,就是node_modules
不稳定,不能放到 Git,也容易被删除。 - 在包目录下创建一个
types
目录,专门用来管理自己写的声明文件。目录结构和上面一致,这个方案需要配置tsconfig.json
中的paths
和baseUrl
字段。
export
npm 包的声明文件和全局变量的声明文件有一些区别。在 npm 包的声明文件中,declare
关键字不会再声明一个全局变量,只会声明一个局部变量,局部变量只有使用 export
进行导出后,才能被 import
进行导入。
export
语法和普通的 TypeScript 代码中使用方法类似,有一个区别就是声明文件中禁止具体的实现:
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;}
// ==============================
import {name, getName, Animal, Directions, Options} from 'foo';
可以使用 declare
先声明多个变量,最后使用 export
一次性导出:
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 namespace
与 declare namespace
类似,export namespace
用来导出一个拥有子属性的对象:
export namespace foo {
const name: string;
namespace bar {function baz(): string;
}
}
// ============================
import {foo} from 'foo';
console.log(foo.name);
foo.bar.baz();
export default
export default
用来导出一个默认值:
export default function foo(): string;
// ==================================
import foo from 'foo';
只有 function
、class
、interface
支持直接默认导出,其他的变量需要先定义才能默认导出:
// ERROR: Expression expected.
export default enum Directions {
Up,
Down,
Left,
Right
}
// SUCCESS
declare enum Directions {
Up,
Down,
Left,
Right
}
export default Directions;
export =
NodeJS 主要是使用 commonjs
规范进行导出模块,:
// 整体导出
module.exports = foo;
// 单个导出
exports.bar = bar;
在 TypeScript 中,对于这种导出的模块,有很多方式可以进行导入:
// === const ... = require, 这也是 NodeJS 使用的方式
// 整体导入
const foo = require('foo');
// 单个导入
const bar = require('foo').bar;
const {bar} = require('foo');
// === import ... from, 这也是 ES6 支持的方式
// 整体导入
import * as foo from 'foo';
// 单个导入
import {bar} from 'foo';
// === import ... require, TypeScript 官方推荐的方法
// 整体导入
import foo = require('foo');
// 单个导入
import bar = foo.bar;
对于这种方式导出模块的库,在写声明文件的时候就需要用到 export =
这个语法:
export = foo;
declare function foo(): string;
declare namespace foo {const bar: number;}
需要注意的是,使用了 export =
以后,就不能再使用 export {...}
进行单个导出,这种情况下一般都是通过声明合并,使用 declare namespace foo
来将 bar
合并到 foo
里。
在普通的 TypeScript 文件中也是可以使用 export =
,而且 TypeScript 为了兼容 AMD
规范和 commonjs
规范而支持 import ... require
和 export =
。
更推荐使用 ES6 标准的 export default
和 export
。
UMD 库
UMD 库是指既可以通过 <script>
标签引入,又可以通过 import
导入的库。对于这样的库,需要额外使用 export as namespace
声明一个全局变量,
一般使用 export as namespace
时,都是先有了 npm 包的声明文件,再基于它添加一条 export as namespace
语句,就可以将声明好的一个变量声明未全局变量:
export as namespace foo;
// 支持 export =, export default
export = foo;
export default foo;
declare function foo(): string;
declare namespace foo {const bar: number;}
直接扩展全局变量
在一些第三方库文件中,有时候会扩展全局变量的属性,但是全局变量的类型却没有更新,这时候就会导致 TypeScript 编译错误,可以在声明文件中扩展全局变量的类型来避免这种情况:
// 通过声明合并,使用 interface String 给 String 添加属性或方法
interface String {sayHello(): string;
}
'foo'.sayHello();
也可以使用 declare namespace
给已有的命名空间添加类型声明:
declare namespace JQuery {
interface CustomOptions {bar: string;}
}
interface JQueryStatic {foo(options: JQuery.CustomOptions): string;
}
// ===========================================
jQuery.foo({bar: ''});
在 npm 包或 UMD 库中扩展全局变量
对于一个 npm 包或者 UMD 库的声明文件,只有 export
导出的类型声明才能被导入。所以对于 npm 包或 UMD 库,如果导入此库之后会扩展全局变量,则需要使用另一种语法在声明文件中扩展全局变量的类型,那就是 declare global
:
declare global {
interface String {sayHello(): string;
}
}
export {};
// =======================
'bar'.sayHello();
注意即使此声明文件不需要导出任何东西,仍然需要导出一个空对象,用来告诉编译器这是一个模块的声明文件,而不是一个全局变量的声明文件。
模块插件
有时通过 import
导入一个模块插件,可以改变另一个原有模块的结构。此时如果原有模块已经有了类型声明文件,而插件模块没有类型声明文件,就会导致类型不完整,缺少插件部分的类型。TypeScript 提供了一个语法 declare module
,它可以用来扩展原有模块的类型。
如果是需要扩展原有模块的话,需要在类型声明文件中先引用原有模块,再使用 declare module
扩展原有模块:
import * as moment from 'moment';
declare module 'moment' {export function foo(): moment.CalendarKey;
}
declare module
也可用于在一个文件中一次性声明多个模块的类型:
declare module 'foo' {
export interface Foo {foo: string;}
}
declare module 'bar' {export function bar(): string;
}
// ==============================
import {Foo} from 'foo';
import * as bar from 'bar';
let f: Foo;
bar.bar();
声明文件中的依赖
一个声明文件有时会依赖另一个声明文件中的类型,比如在前面的 declare module
的例子中,声明文件中就导入了 moment
,并且使用了 moment.CalendarKey
这个类型:
import * as moment from 'moment';
declare module 'moment' {export function foo(): moment.CalendarKey;
}
除了可以在声明文件中通过 import
导入另一个声明文件中的类型之外,还有一个语法也可以用来导入另一个声明文件,那就是三斜线指令。
与 namespace
类似,三斜线指令也是 TypeScript 在早期版本中为了描述模块之间的依赖关系而创造的语法。随着 ES6 的广泛应用,现在已经不建议再使用 TypeScript 中的三斜线指令来声明模块之间的依赖关系了。
类似于声明文件中的 import
,它可以用来导入另一个声明文件。与 import
的区别是,当且仅当在以下几个场景下,我们才需要使用三斜线指令替代 import
:
- 我们在书写一个全局变量的声明文件时。
- 当我们需要依赖一个全局变量的声明文件时。
/// <reference types="..." />
自动生成声明文件
如果库的源码本身就是由 TypeScript 写的,那么在使用 tsc
脚本将 TypeScript 编译为 JavaScript 的时候,添加 declaration
选项,就可以同时也生成 .d.ts
声明文件了。
可以在命令行中添加 --declaration
(简写 -d
),或者在 tsconfig.json
中添加 declaration
选项:
{
"compilerOptions": {
"module": "commonjs",
"outDir": "lib",
"declaration": true,
}
}
使用 tsc
自动生成声明文件时,每个 TypeScript 文件都会对应一个 .d.ts
声明文件。这样的好处是,使用方不仅可以在使用 import foo from 'foo'
导入默认的模块时获得类型提示,还可以在使用 import bar from 'foo/lib/bar'
导入一个子模块时,也获得对应的类型提示。
除了 declaration
选项之外,还有几个选项也与自动生成声明文件有关,这里只简单列举出来,不做详细演示了:
-
declarationDir
设置生成.d.ts
文件的目录。 -
declarationMap
对每个.d.ts
文件,都生成对应的.d.ts.map
(sourcemap)文件。 -
emitDeclarationOnly
仅生成.d.ts
文件,不生成.js
文件。