为什么需要声明?
声明的本质是告知编译器一个标识符的类型信息。同时,在使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
声明在TypeScript中至关重要,只有通过声明才能告知编译器这个标识符到底代表什么含义。对于语言关键字之外的任意标识符,如果编译器无法获取它的声明,将会报错:
// 错误,凭空出现的variable, 编译器无法知道它代表什么含义
// error TS2304: Cannot find name 'variable'
console.log(variable);
改正这个错误,需要添加 variable
的声明信息:
// 声明语句
let variable: number;
// 正确,已声明variable为数字
console.log(variable);
虽然编译成JavaScript代码执行时仍然会报错,但因为添加了variable
的声明信息,在TypeScript中逻辑合理,不会报错。
内部声明
到目前为止,所有在TypeScript源码(ts/tsx结尾的文件,d.ts不算)中出现的声明,都是内部声明:
// 声明a为一个数字
let a: number;
// 声明b为一个数字并初始化为2
let b: number = 2;
// 声明T为一个接口
interface T {}
// 声明接口类型变量b
let b: T;
// 声明fn为一个函数
function fn(){}
// 声明myFunc为一个函数
// 此处利用了类型推导
let myFunc = function(a: number){}
// 声明MyEnum枚举类型
enum MyEnum {
A, B
}
// 声明NS为命名空间
namespace NS {}
// ...
内部声明主要是你当前所写的代码中的所有变量和类型的声明。
外部声明
外部声明一般针对第三方来历不明的库,当你想要在你的typescript项目中使用用javascript代码写的第三方库时,就需要用到外部声明。一个常见的例子,假设我们在HTML中通过script
标签引入了全局jQuery
:
// 注册全局变量 $
<script src="path/to/jquery.js"></script>
path/to/jquery.js
文件在会在全局作用域中引入对象 $
,接下来如果在同一项目下的TypeScript文件中使用 $
,TypeScript编译器会报错:
// 错误,缺少名字 $ 的声明信息
// error TS2581: Cannot find name '$'. Do you need to install type definitions for jQuery? Try `npm i @types/jquery`
$('body').html('hello world');
由于没有任何类型信息,TypeScript编译器根本不知道 $
代表的是什么,此时需要引入外部声明(因为$
是外部JavaScript引入TypeScript代码中的)。外部声明的关键字是:declare
分析语句 $('body').html('hello world');
得出:
-
$
是一个函数,接收字符串参数 -
$
调用返回值是一个对象,此对象拥有成员函数html
,这个成员函数的参数也是字符串类型
// 声明 $ 的类型信息
declare let $: (selector: string) => {
html: (content: string) => void;
};
// 正确,$已经通过外部声明
$('body').html('hello world');
声明应该是纯粹对于一个标识符类型或外观的描述,便于编译器识别,外部声明具有以下特点:
- 必须使用
declare
修饰外部声明 - 不能包含实现或初始化信息(内部声明可以在声明的时候包含实现或初始化)
// 声明a为一个数字
declare let a: number;
// 错误,外部声明不能初始化
// error TS1039: Initializers are not allowed in ambient contexts
declare let b: number = 2;
// 声明T为一个接口
declare interface T {}
// 声明接口类型变量b
let b: T;
// 声明fn为一个函数
// 错误,声明包含了函数实现
// error TS1183: An implementation cannot be declared in ambient contexts
declare function fn(){}
// 正确,不包含函数体实现
declare function fn(): void;
// 声明myFunc为一个函数
declare let myFunc: (a: number) => void;
// 声明MyEnum枚举类型
declare enum MyEnum {
A, B
}
// 声明NS为命名空间
declare namespace NS {
// 错误,声明不能初始化
// error TS1039: Initializers are not allowed in ambient contexts
const a: number = 1;
// 正确,仅包含声明
const b: number;
// 正确,函数未包含函数体实现
function c(): void;
}
// 声明一个类
declare class Greeter {
constructor(greeting: string);
greeting: string;
showGreeting(): void;
}
外部声明还可以用于声明一个模块,如果一个外部模块的成员要被外部访问,模块成员应该用 export
声明导出:
declare module 'io' {
export function read(file: string): string;
export function write(file: string, data: string): void;
}
声明文件
通常我们会把声明语句放到一个单独的文件(jQuery.d.ts
)中,这就是声明文件。
声明文件必需以
.d.ts
为后缀
一般来说,ts 会解析项目中所有的 *.ts
文件,当然也包含以 .d.ts
结尾的文件。所以当我们将 jQuery.d.ts
放到项目中时,其他所有 *.ts
文件就都可以获得 jQuery
的类型定义了。
/path/to/project
├── src
| ├── index.ts
| └── jQuery.d.ts
└── tsconfig.json
假如仍然无法解析,那么可以检查下 tsconfig.json
中的 files
、include
和 exclude
配置,确保其包含了 jQuery.d.ts
文件。
这里只演示了全局变量这种模式的声明文件,假如是通过模块导入的方式使用第三方库的话,那么引入声明文件又是另一种方式了。
当一个第三方库没有提供声明文件时,我们就需要自己书写声明文件了。前面只介绍了最简单的声明文件内容,而真正书写一个声明文件并不是一件简单的事,以下会详细介绍如何书写声明文件。
在不同的场景下,声明文件的内容和使用方式会有所区别。
库的使用场景主要有以下几种:
- 全局变量:通过 `` 标签引入第三方库,注入全局变量
-
npm 包:通过
import foo from 'foo'
导入,符合 ES6 模块规范 -
UMD 库:既可以通过 `` 标签引入,又可以通过
import
导入 - 直接扩展全局变量:通过 `` 标签引入后,改变一个全局变量的结构
- 在 npm 包或 UMD 库中扩展全局变量:引用 npm 包或 UMD 库后,改变一个全局变量的结构
-
模块插件:通过 `` 或
import
导入后,改变另一个模块的结构
下面主要介绍全局变量的方式:
全局变量是最简单的一种场景,之前举的例子就是通过 标签引入 jQuery,注入全局变量
$和
jQuery。
使用全局变量的声明文件时,如果是以 npm install @types/xxx --save-dev
安装的,则不需要任何配置。如果是将声明文件直接存放于当前项目中,则建议和其他源码一起放到 src
目录下(或者对应的源码目录下):
/path/to/project
├── src
| ├── index.ts
| └── jQuery.d.ts
└── tsconfig.json
如果没有生效,可以检查下 tsconfig.json
中的 files
、include
和 exclude
配置,确保其包含了 jQuery.d.ts
文件。
全局变量的声明文件主要有以下几种语法:
-
declare var
声明全局变量 -
declare function
声明全局方法 -
declare class
声明全局类 -
declare enum
声明全局枚举类型 -
declare namespace
声明(含有子属性的)全局对象 -
interface
和type
声明全局类型
declare var
在所有的声明语句中,declare var
是最简单的,如之前所学,它能够用来定义一个全局变量的类型。与其类似的,还有 declare let
和 declare const
,使用 let
与使用 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);
};
而当我们使用 const
定义时,表示此时的全局变量是一个常量,不允许再去修改它的值了4:
// src/jQuery.d.ts
declare const jQuery: (selector: string) => any;
jQuery('#foo');
// 使用 declare const 定义的 jQuery 类型,禁止修改这个全局变量
jQuery = function(selector) {
return document.querySelector(selector);
};
// ERROR: Cannot assign to 'jQuery' because it is a constant or a read-only property.
一般来说,全局变量都是禁止修改的常量,所以大部分情况都应该使用 const
而不是 var
或 let
。
需要注意的是,声明语句中只能定义类型,切勿在声明语句中定义具体的实现5:
declare const jQuery = function(selector) {
return document.querySelector(selector);
};
// ERROR: An implementation cannot be declared in ambient contexts.
declare function
declare function
用来定义全局函数的类型。jQuery 其实就是一个函数,所以也可以用 function
来定义:
// src/jQuery.d.ts
declare function jQuery(selector: string): any;
// src/index.ts
jQuery('#foo');
在函数类型的声明语句中,函数重载也是支持的6:
// src/jQuery.d.ts
declare function jQuery(selector: string): any;
declare function jQuery(domReadyCallback: () => any): any;
// src/index.ts
jQuery('#foo');jQuery(function() {
alert('Dom Ready!');
});
declare class
当全局变量是一个类的时候,我们用 declare class
来定义它的类型7:
// src/Animal.d.ts
declare class Animal {
name: string;
constructor(name: string);
sayHi(): string;
}
// src/index.ts
let cat = new Animal('Tom');
同样的,declare class
语句也只能用来定义类型,不能用来定义具体的实现,比如定义 sayHi
方法的具体实现则会报错:
// src/Animal.d.ts
declare class Animal {
name: string;
constructor(name: string);
sayHi() {
return `My name is ${this.name}`;
};
// ERROR: An implementation cannot be declared in ambient contexts.
}
declare enum
使用 declare enum
定义的枚举类型也称作外部枚举(Ambient Enums),举例如下8:
// 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 enum
仅用来定义类型,而不是具体的值。
Directions.d.ts
仅仅会用于编译时的检查,声明文件里的内容在编译结果中会被删除。它编译结果是:
var directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
其中 Directions
是由第三方库定义好的全局变量。
declare namespace
namespace
是 ts 早期时为了解决模块化而创造的关键字,中文称为命名空间。
由于历史遗留原因,在早期还没有 ES6 的时候,ts 提供了一种模块化方案,使用 module
关键字表示内部模块。但由于后来 ES6 也使用了 module
关键字,ts 为了兼容 ES6,使用 namespace
替代了自己的 module
,更名为命名空间。
随着 ES6 的广泛应用,现在已经不建议再使用 ts 中的 namespace
,而推荐使用 ES6 的模块化方案了,故我们不再需要学习 namespace
的使用了。
namespace
被淘汰了,但是在声明文件中,declare namespace
还是比较常用的,它用来表示全局变量是一个对象,包含很多子属性。
比如 jQuery
是一个全局变量,它是一个对象,提供了一个 jQuery.ajax
方法可以调用,那么我们就应该使用 declare namespace jQuery
来声明这个拥有多个子属性的全局变量。
// src/jQuery.d.ts
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
}
// src/index.ts
jQuery.ajax('/api/get_something');
注意,在 declare namespace
内部,我们直接使用 function ajax
来声明函数,而不是使用 declare function ajax
。类似的,也可以使用 const
, class
, enum
等语句9:
// 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/index.ts
jQuery.ajax('/api/get_something');
console.log(jQuery.version);
const e = new jQuery.Event();
e.blur(jQuery.EventType.CustomClick);
嵌套的命名空间
如果对象拥有深层的层级,则需要用嵌套的 namespace
来声明深层的属性的类型10:
// src/jQuery.d.ts
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
namespace fn {
function extend(object: any): void;
}
}
// src/index.ts
jQuery.ajax('/api/get_something');
jQuery.fn.extend({
check: function() {
return this.each(function() {
this.checked = true;
});
}
});
假如 jQuery
下仅有 fn
这一个属性(没有 ajax
等其他属性或方法),则可以不需要嵌套 namespace
11:
// src/jQuery.d.ts
declare namespace jQuery.fn {
function extend(object: any): void;
}
// src/index.ts
jQuery.fn.extend({
check: function() {
return this.each(function() {
this.checked = true;
});
}
});
interface 和 type
除了全局变量之外,可能有一些类型我们也希望能暴露出来。在类型声明文件中,我们可以直接使用 interface
或 type
来声明一个全局的接口或类型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);
type
与 interface
类似,不再赘述。
防止命名冲突
暴露在最外层的 interface
或 type
会作为全局类型作用于整个项目中,我们应该尽可能的减少全局变量或全局类型的数量。故最好将他们放到 namespace
下13:
// src/jQuery.d.ts
declare namespace jQuery {
interface AjaxSettings {
method?: 'GET' | 'POST'
data?: any;
}
function ajax(url: string, settings?: AjaxSettings): void;
}
注意,在使用这个 interface
的时候,也应该加上 jQuery
前缀:
// src/index.ts
let settings: jQuery.AjaxSettings = {
method: 'POST',
data: {
name: 'foo'
}};
jQuery.ajax('/api/post_something', settings);
声明合并
假如 jQuery 既是一个函数,可以直接被调用 jQuery('#foo')
,又是一个对象,拥有子属性 jQuery.ajax()
(事实确实如此),那么我们可以组合多个声明语句,它们会不冲突的合并起来14:
// src/jQuery.d.ts
declare function jQuery(selector: string): any;
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
}
// src/index.ts
jQuery('#foo');jQuery.ajax('/api/get_something');
关于声明合并的更多用法,可以查看声明合并章节。
总结
声明的本质是告知编译器一个标识符的类型信息。相应的声明文件能让我们在使用第三方库时便于获得对应的代码补全、接口提示等功能。
以上内容只是部分讲解,主要引导不懂d.ts
文件的同学知道其用途,想知道如何编写声明文件请查询具体文档,见参考部分。