# 声明相关
# 什么是声明语句
假如,想使用第三方库 jQuery,一种常见的方式是在 html 中通过 <script>
标签引入 jQuery,然后就可以使用全局变量 $
或 jQuery
了。
通常这样获取一个 id
是 foo
的元素:
$('#foo');
// or
jQuery('#foo');
但是在 ts 中,编译器并不知道 $
或 jQuery
是什么东西:
jQuery('#foo');
// ERROR: Cannot find name 'jQuery'.
这时,需要使用 declare var
来定义它的类型:
declare var jQuery: (selector: string) => any;
jQuery('#foo');
上例中,declare var
并没有真的定义一个变量,只是定义了全局变量 jQuery
的类型,仅仅会用于编译时的检查,在编译结果中会被删除。
它编译结果是:
jQuery('#foo');
除了 declare var
之外,还有其他很多种声明语句,将会在后面详细介绍。
# 声明文件
# 什么是声明文件
通常会把声明语句放到一个单独的文件(jQuery.d.ts
)中,这就是声明文件:
// src/jQuery.d.ts
declare var jQuery: (selector: string) => any;
// src/index.ts
jQuery('#foo');
声明文件必需以 .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
文件。
这里只演示了全局变量这种模式的声明文件,假如是通过模块导入的方式使用第三方库,怎么办呢?见下面
# 第三方声明文件
当然,jQuery 的声明文件不需要定义了,社区已经帮定义好了:jQuery in DefinitelyTyped (opens new window)。
可以直接下载下来使用,但是更推荐的是使用 @types
统一管理第三方库的声明文件。
@types
的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例:
npm install @types/jquery --save-dev
可以在这个页面 (opens new window)找到官方的工具,还可以找到tsconfig配置文件的参考文档
在Type Search (opens new window)搜索声明文件:
小知识:既可以通过 <script>
标签引入,又可以通过 import
导入的库,称为 UMD 库。e.g: JQuery其实就是一个UMD的库!
相比于 npm 包的类型声明文件,需要额外声明一个全局变量,为了实现这种方式,ts 提供了一个新语法 export as namespace
。
一般使用 export as namespace
时,都是先有了 npm 包的声明文件,再基于它添加一条 export as namespace
语句,即可将声明好的一个变量声明为全局变量,举例如下:
// types/foo/index.d.ts
export as namespace foo;
export = foo;
declare function foo(): string;
declare namespace foo {
const bar: number;
}
当然它也可以与 export default
一起使用:
// types/foo/index.d.ts
export as namespace foo;
export default foo;
declare function foo(): string;
declare namespace foo {
const bar: number;
}
对于一个 npm 包或者 UMD 库的声明文件,只有 export
导出的类型声明才能被导入。
所以对于 npm 包或 UMD 库,如果导入此库之后会扩展全局变量,则需要使用另一种语法在声明文件中扩展全局变量的类型,那就是 declare global。
# 书写声明文件
全局变量是最简单的一种场景,之前举的例子就是通过 <script>
标签引入 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
声明(含有子属性的)全局对象,传送门
# declare var
在所有的声明语句中,declare var
是最简单的,如之前所学,它能够用来定义一个全局变量的类型。与其类似的,还有 declare let
和 declare const
,使用 let
与使用 var
没有什么区别:
// src/jQuery.d.ts
declare let jQuery: (selector: string) => any;
# declare function
declare function
用来定义全局函数的类型。
示例:jQuery 其实就是一个函数,所以也可以用 function
来定义:
// src/jQuery.d.ts
declare function jQuery(selector: string): any;
// src/index.ts
jQuery('#foo');
// 在函数类型的声明语句中,函数重载也是支持的:
// 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
来定义它的类型:
// src/Animal.d.ts
declare class Animal {
name: string;
constructor(name: string);
sayHi(): string;
}
// src/index.ts
let cat = new Animal('Tom');
// declare class 语句也只能用来定义类型,不能用来定义具体的实现
// 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 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 (opens new window):
// 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
来声明深层的属性的类型:
// 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;
});
}
});
# declare module
有时通过 import
导入一个模块插件,可以改变另一个原有模块的结构。
此时如果原有模块已经有了类型声明文件,而插件模块没有类型声明文件,就会导致类型不完整,缺少插件部分的类型。
ts 提供了一个语法 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();
declare module
也可用于在一个文件中一次性声明多个模块的类型:
// types/foo-bar.d.ts
declare module 'foo' {
export interface Foo {
foo: string;
}
}
declare module 'bar' {
export function bar(): string;
}
// src/index.ts
import { Foo } from 'foo';
import * as bar from 'bar';
let f: Foo;
bar.bar();
# declare global
使用 declare global
可以在 npm 包或者 UMD 库的声明文件中扩展全局变量的类型:
// types/foo/index.d.ts
declare global {
interface String {
prependHello(): string;
}
}
export {};
// src/index.ts
'bar'.prependHello();
注意即使此声明文件不需要导出任何东西,仍然需要导出一个空对象,用来告诉编译器这是一个模块的声明文件,而不是一个全局变量的声明文件。
# 发布声明文件
当我们为一个库写好了声明文件之后,下一步就是将它发布出去了。
此时有两种方案:
将声明文件和源码放在一起
package.json
中的types
或typings
字段指定一个类型声明文件地址。比如:{ "name": "foo", "version": "1.0.0", "main": "lib/index.js", "types": "foo.d.ts", }
将声明文件发布到
@types
下如果我们是在给别人的仓库添加类型声明文件,但原作者不愿意合并 pull request,那么就需要将声明文件发布到
@types
下。与普通的 npm 模块不同,
@types
是统一由 DefinitelyTyped (opens new window) 管理的。要将声明文件发布到@types
下,就需要给 DefinitelyTyped (opens new window) 创建一个 pull-request,其中包含了类型声明文件,测试代码,以及tsconfig.json
等。pull-request 需要符合它们的规范,并且通过测试,才能被合并,稍后就会被自动发布到
@types
下。在 DefinitelyTyped (opens new window) 中创建一个新的类型声明,需要用到一些工具,DefinitelyTyped (opens new window) 的文档中已经有了详细的介绍 (opens new window),这里就不赘述了,以官方文档为准。
这两种方案中优先选择第一种方案。
保持声明文件与源码在一起,使用时就不需要额外增加单独的声明文件库的依赖了,而且也能保证声明文件的版本与源码的版本保持一致。
仅当我们在给别人的仓库添加类型声明文件,但原作者不愿意合并 pull request 时,才需要使用第二种方案,将声明文件发布到 @types
下。
# 生成声明文件
方案有三:
使用tsc生成
可以在命令行中添加
--declaration
(简写-d
),或者在tsconfig.json
中添加declaration
选项。这里以tsconfig.json
为例:{ "compilerOptions": { "module": "commonjs", "outDir": "lib", "declaration": true, } }
上例中我们添加了
outDir
选项,将 ts 文件的编译结果输出到lib
目录下,然后添加了declaration
选项,设置为true
,表示将会由 ts 文件自动生成.d.ts
声明文件,也会输出到lib
目录下。运行
tsc
之后,目录结构如下:/path/to/project ├── lib | ├── bar | | ├── index.d.ts | | └── index.js | ├── index.d.ts | └── index.js ├── src | ├── bar | | └── index.ts | └── index.ts ├── package.json └── tsconfig.json
除了
declaration
选项之外,还有几个选项也与自动生成声明文件有关,这里只简单列举出来,不做详细演示了:declarationDir
设置生成.d.ts
文件的目录declarationMap
对每个.d.ts
文件,都生成对应的.d.ts.map
(sourcemap)文件emitDeclarationOnly
仅生成.d.ts
文件,不生成.js
文件
使用npm工具:dts-gen (opens new window)
# 声明合并
声明合并是指 TypeScript 编译器会将名字相同的多个声明合并为一个声明,合并后的声明同时拥有多个声明的特性。
知道在 JavaScrip 中,使用var关键字定义变量时,定义相同名字的变量,后面的会覆盖前面的值。
使用let 定义变量和使用 const 定义常量时,不允许名字重复。
在 TypeScript 中,接口、命名空间是可以多次声明的,最后 TypeScript 会将多个同名声明合并为一个。
下来看个简单的例子:
interface Info {
name: string
}
interface Info {
age: number
}
let info: Info
info = { // error 类型“{ name: string; }”中缺少属性“age”
name: 'lison'
}
info = { // right
name: 'lison',
age: 18
}
可以看到,定义了两个同名接口Info,每个接口里都定义了一个必备属性,最后定义info类型为Info时,info的定义要求同时包含name和age属性。
这就是声明合并的简单示例。
TypeScript的所有声明概括起来,会创建这三种实体之一:命名空间、类型和值:
- 命名空间的创建实际是创建一个对象,对象的属性是在命名空间里export导出的内容;
- 类型的声明是创建一个类型并赋给一个名字;
- 值的声明就是创建一个在JavaScript中可以使用的值。
下面这个表格会清晰的告诉你,每一种声明类型会创建这三种实体中的哪种。
先来说明一下,第一列是指声明的内容,每一行包含4列,表明这一行中,第一列的声明类型创建了后面三列哪种实体,打钩即表示创建了该实体:
声明类型 | 创建了命名空间 | 创建了类型 | 创建了值 |
---|---|---|---|
Namespace | √ | √ | |
Class | √ | √ | |
Enum | √ | √ | |
Interface | √ | ||
Type Alias类型别名 | √ | ||
Function | √ | ||
Variable | √ |
可以看到:
只要命名空间创建了命名空间这种实体。
Class、Enum两个,Class即是实际的值也作为类使用,Enum编译为JavaScript后也是实际值,而且一定条件下,它的成员可以作为类型使用;
Interface和类型别名是纯粹的类型;
而Funciton和Variable只是创建了JavaScript中可用的值,不能作为类型使用,注意这里Variable是变量,不是常量,常量是可以作为类型使用的。
# 合并函数
可以使用重载定义多个函数类型:
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
# 合并接口(最常用)
多个同名接口,定义的非函数的成员命名应该是不重复的,如果重复了,类型应该是相同的,否则将会报错。
interface Info {
name: string
}
interface Info {
age: number
}
interface Info {
age: boolean // error 后续属性声明必须属于同一类型。属性“age”的类型必须为“number”,但此处却为类型“boolean”
}
对于函数成员,每个同名函数成员都会被当成这个函数的重载,且合并时后面的接口具有更高的优先级。来看下多个同名函数成员的例子:
interface Res {
getRes(input: string): number
}
interface Res {
getRes(input: number): string
}
const res: Res = {
getRes: (input: any): any => {
if (typeof input === 'string') return input.length
else return String(input)
}
}
res.getRes('123').length // error 类型“number”上不存在属性“length”
# 合并命名空间
同名命名空间最后会将多个命名空间导出的内容进行合并,如下面两个命名空间:
namespace Validation {
export const checkNumber = () => {}
}
namespace Validation {
export const checkString = () => {}
}
上面定义两个同名命名空间,效果相当于:
namespace Validation {
export const checkNumber = () => {}
export const checkString = () => {}
}
在命名空间里,有时并不是把所有内容都对外部可见,对于没有导出的内容,在其它同名命名空间内是无法访问的:
namespace Validation {
const numberReg = /^[0-9]+$/
export const stringReg = /^[A-Za-z]+$/
export const checkString = () => {}
}
namespace Validation {
export const checkNumber = (value: any) => {
return numberReg.test(value) // error 找不到名称“numberReg”
}
}
上面定义的两个命名空间,numberReg没有使用export导出,所以在第二个同名命名空间内是无法使用的,如果给 const numberReg 前面加上 export,就可以在第二个命名空间使用了。
命名空间分别和类、函数、枚举都可以合并,下面来一一说明:
# 命名空间和类
这里要求同名的类和命名空间在定义的时候,类的定义必须在命名空间前面,最后合并之后的效果,一个包含一些以命名空间导出内容为静态属性的类,来看例子:
class Validation {
checkType() { }
}
namespace Validation {
export const numberReg = /^[0-9]+$/
export const stringReg = /^[A-Za-z]+$/
export const checkString = () => { }
}
namespace Validation {
export const checkNumber = (value: any) => {
return numberReg.test(value)
}
}
console.log(Validation.prototype) // { checkType: fun () {} }
console.log(Validation.prototype.constructor)
/**
{
checkNumber: ...
checkString: ...
numberReg: ...
stringReg: ...
}
*/
# 命名空间和函数
在JavaScript中,函数也是对象,所以可以给一个函数设置属性,在TypeScript中,就可以通过声明合并实现。
但同样要求,函数的定义要在同名命名空间前面,再拿之前讲过的计数器的实现来看下,如何利用声明合并实现计数器的定义:
function countUp () {
countUp.count++
}
namespace countUp {
export let count = 0
}
countUp()
countUp()
console.log(countUp.count) // 2
# 命名空间和枚举
可以通过命名空间和枚举的合并,为枚举拓展内容,枚举和同名命名空间的先后顺序是没有要求的,来看例子:
enum Colors {
red,
green,
blue
}
namespace Colors {
export const yellow = 3
}
console.log(Colors)
/*
{
0: "red",
1: "green",
2: "blue",
red: 0,
green: 1,
blue: 2,
yellow: 3
}
*/
通过打印结果你可以发现,虽然使用命名空间增加了枚举的成员,但是最后输出的值只有key到index的映射,没有index到key的映射。