声明语法
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 />
三斜线指令
声明文件
通常我们会把声明语句放到一个单独的文件(jQuery.d.ts
)中,这就是声明文件.
声明只能用来定义类型,不能用来定义具体的实现.
// src/jQuery.d.ts
declare var jQuery: (selector: string) => any;
声明文件必需以 .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
文件。
第三方声明文件
推荐的是使用 @types
统一管理第三方库的声明文件。@types
的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例:
npm install @types/jquery --save-dev
使用typescriptlang search页面搜索你需要的声明文件
通常我们会把声明语句放到一个单独的文件(jQuery.d.ts
)中,这就是声明文件:
// src/jQuery.d.ts
// 全局变量这种模式的声明文件
declare var jQuery: (selector: string) => any;
库的使用场景
当一个第三方库没有提供声明文件时,我们就需要自己书写声明文件.
库的使用场景主要有以下几种:
全局变量
:通过<script>
标签引入第三方库,注入全局变量npm
包:通过import foo from 'foo'
导入,符合 ES6 模块规范UMD
库:既可以通过<script>
标签引入,又可以通过import
导入- 直接扩展全局变量:通过
<script>
标签引入后,改变一个全局变量的结构 - 在
npm
包或UMD
库中扩展全局变量:引用npm
包或UMD
库后,改变一个全局变量的结构 - 模块插件:通过
<script>
或import
导入后,改变另一个模块的结构
全局变量
全局变量是最简单的一种场景,之前举的例子就是通过 <script>
标签引入 jQuery,注入全局变量 $
和 jQuery
。
使用全局变量的声明文件时,如果是以 npm install @types/xxx --save-dev
安装的,则不需要任何配置。如果是将声明文件直接存放于当前项目中,则建议和其他源码一起放到 src
目录下
/path/to/project
├── src
| ├── index.ts
| └── jQuery.d.ts
└── tsconfig.json
全局变量的声明文件主要有以下几种语法:
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;
一般来说,全局变量都是禁止修改的常量,所以大部分情况都应该使用 const
而不是 var
或 let
。
declare function
declare function
用来定义全局函数的类型。jQuery 其实就是一个函数,所以也可以用 function
来定义:
// src/jQuery.d.ts
declare function jQuery(selector: string): any;
在函数类型的声明语句中,函数重载也是支持的:
// src/jQuery.d.ts
declare function jQuery(selector: string): any;
declare function jQuery(domReadyCallback: () => any): any;
declare class
当全局变量是一个类的时候,我们用 declare class 来定义它的类型:
// src/Animal.d.ts
declare class Animal {
name: string;
constructor(name: string);
sayHi(): string;
}
declare enum
使用 declare enum
定义的枚举类型也称作外部枚举(Ambient Enums
)
// src/Directions.d.ts
declare enum Directions {
Up,
Down,
Left,
Right
}
与其他全局变量的类型声明一致,declare enum
仅用来定义类型,而不是具体的值。
declare namespace
namespace
被淘汰了,但是在声明文件中,declare 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
}
}
嵌套的命名空间
// src/jQuery.d.ts
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
namespace fn {
function extend(object: any): void;
}
}
interface
和 type
除了全局变量之外,可能有一些类型我们也希望能暴露出来。在类型声明文件中,我们可以直接使用 interface
或 type
来声明一个全局的接口或类型
// src/jQuery.d.ts
interface AjaxSettings {
method?: 'GET' | 'POST'
data?: any;
}
declare namespace jQuery {
function ajax(url: string, settings?: AjaxSettings): void;
}
防止命名冲突
暴露在最外层的 interface 或 type 会作为全局类型作用于整个项目中,我们应该尽可能的减少全局变量或全局类型的数量。故最好将他们放到 namespace 下:
// src/jQuery.d.ts
declare namespace jQuery {
interface AjaxSettings {
method?: 'GET' | 'POST'
data?: any;
}
function ajax(url: string, settings?: AjaxSettings): void;
}
声明合并
假如 jQuery
既是一个函数,可以直接被调用 jQuery('#foo')
,又是一个对象,拥有子属性 jQuery.ajax()
,那么我们可以组合多个声明语句,它们会不冲突的合并起来
// src/jQuery.d.ts
declare function jQuery(selector: string): any;
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
}
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
里了。
假如以上两种方式都没有找到对应的声明文件,那么我们就需要自己为它写声明文件了。
创建一个 types
目录,专门用来管理自己写的声明文件,将 foo
的声明文件放到 types/foo/index.d.ts
中。这种方式需要配置下 tsconfig.json
中的 paths
和 baseUrl
字段。
/path/to/project
├── src
| └── index.ts
├── types
| └── foo
| └── index.d.ts
└── tsconfig.json
tsconfig.json
内容:
{
"compilerOptions": {
"module": "commonjs",
"baseUrl": "./",
"paths": {
"*": ["types/*"]
}
}
}
npm 包的声明:
export
导出变量export namespace
导出(含有子属性的)对象export default
ES6 默认导出export =
commonjs 导出模块
export
npm 包的声明文件与全局变量的声明文件有很大区别。在 npm 包的声明文件中,使用 declare
不再会声明一个全局变量,而只会在当前文件中声明一个局部变量。只有在声明文件中使用 export
导出,然后在使用方 import
导入后,才会应用到这些类型声明。
只要
.ts
或.d.ts
文件中有import
或export
,那么这个文件中的declare
就会变成局部变量
export
的语法与普通的 ts 中的语法类似,区别仅在于声明文件中禁止定义具体的实现:
// 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;
}
// src/index.ts
import { name, getName, Animal, Directions, Options } from "foo";
console.log(name);
let myName = getName();
let cat = new Animal("Tom");
let directions = [
Directions.Up,
Directions.Down,
Directions.Left,
Directions.Right,
];
let options: Options = {
data: {
name: "foo",
},
};
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
在 ES6 模块系统中,使用 export default
可以导出一个默认值,使用方可以用 import foo from 'foo'
而不是 import { foo } from 'foo'
来导入这个默认值。
在类型声明文件中,export default
用来导出默认值的类型:
// types/foo/index.d.ts
export default function foo(): string;
// src/index.ts
import foo from "foo";
foo();
注意,只有 function
、class
和 interface
可以直接默认导出,其他的变量需要先定义出来,再默认导出.
export =
在 commonjs 规范中,我们用以下方式来导出一个模块:
// 整体导出
module.exports = foo;
// 单个导出
exports.bar = bar;
导入模块:
import ... require
// 整体导入
import foo = require('foo');
// 单个导入
import bar = require('foo').bar;
UMD 库
既可以通过 <script>
标签引入,又可以通过 import
导入的库,称为 UMD
库。相比于 npm 包的类型声明文件,我们需要额外声明一个全局变量,为了实现这种方式,ts 提供了一个新语法 export as namespace
。
一般使用 export as namespace
时,都是先有了 npm 包的声明文件,再基于它添加一条 export as namespace
语句,即可将声明好的一个变量声明为全局变量,举例如下:
// types/foo/index.d.ts
export as namespace foo;
export = foo;
//export default foo;
declare function foo(): string;
declare namespace foo {
const bar: number;
}
扩展全局变量
直接扩展全局变量
有的第三方库扩展了一个全局变量,可是此全局变量的类型却没有相应的更新过来,就会导致 ts 编译错误,此时就需要扩展全局变量的类型。比如扩展 String
类型:
interface String {
prependHello(): string;
}
"foo".prependHello();
通过声明合并,使用 interface String
即可给 String
添加属性或方法。
也可以使用 declare namespace
给已有的命名空间添加类型声明:
// types/jquery-plugin/index.d.ts
declare namespace JQuery {
interface CustomOptions {
bar: string;
}
}
interface JQueryStatic {
foo(options: JQuery.CustomOptions): string;
}
// src/index.ts
jQuery.foo({
bar: "",
});
在 npm 包或 UMD 库中扩展全局变量
如之前所说,对于一个 npm 包或者 UMD 库的声明文件,只有 export
导出的类型声明才能被导入。所以对于 npm 包或 UMD 库,如果导入此库之后会扩展全局变量,则需要使用另一种语法在声明文件中扩展全局变量的类型,那就是 declare global
declare global
使用 declare global
可以在 npm 包或者 UMD 库的声明文件中扩展全局变量的类型:
// types/foo/index.d.ts
declare global {
interface String {
prependHello(): string;
}
}
export {};
注意即使此声明文件不需要导出任何东西,仍然需要导出一个空对象,用来告诉编译器这是一个模块的声明文件,而不是一个全局变量的声明文件。
// src/index.ts
"bar".prependHello();
模块插件§
有时通过 import
导入一个模块插件,可以改变另一个原有模块的结构。此时如果原有模块已经有了类型声明文件,而插件模块没有类型声明文件,就会导致类型不完整,缺少插件部分的类型。ts 提供了一个语法 declare module
,它可以用来扩展原有模块的类型。
declare module
如果是需要扩展原有模块的话,需要在类型声明文件中先引用原有模块,再使用 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();
自动生成声明文件§
如果库的源码本身就是由 ts 写的,那么在使用 tsc
脚本将 ts 编译为 js 的时候,添加 declaration
选项,就可以同时也生成 .d.ts
声明文件了。
我们可以在命令行中添加 --declaration
(简写 -d),或者在 tsconfig.json
中添加 declaration
选项。这里以 tsconfig.json
为例:
{
"compilerOptions": {
"module": "commonjs",
"outDir": "lib",
"declaration": true
}
}
发布声明文件
当我们为一个库写好了声明文件之后,下一步就是将它发布出去了。
此时有两种方案:
- 将声明文件和源码放在一起
- 将声明文件发布到
@types
下
这两种方案中优先选择第一种方案。保持声明文件与源码在一起,使用时就不需要额外增加单独的声明文件库的依赖了,而且也能保证声明文件的版本与源码的版本保持一致。
仅当我们在给别人的仓库添加类型声明文件,但原作者不愿意合并 pull request
时,才需要使用第二种方案,将声明文件发布到 @types
下。
将声明文件和源码放在一起
如果声明文件是通过 tsc
自动生成的,那么无需做任何其他配置,只需要把编译好的文件也发布到 npm
上,使用方就可以获取到类型提示了。
如果是手动写的声明文件,那么需要满足以下条件之一,才能被正确的识别:
- 给
package.json
中的types
或typings
字段指定一个类型声明文件地址 - 在项目根目录下,编写一个
index.d.ts
文件 - 针对入口文件(
package.json
中的main
字段指定的入口文件),编写一个同名不同后缀的 .d.ts
文件
第一种方式是给 package.json
中的 types
或 typings
字段指定一个类型声明文件地址。比如:
{
"name": "foo",
"version": "1.0.0",
"main": "lib/index.js",
"types": "foo.d.ts"
}
指定了 types
为 foo.d.ts
之后,导入此库的时候,就会去找 foo.d.ts
作为此库的类型声明文件了。
typings
与 types
一样,只是另一种写法。
如果没有指定 types
或 typings
,那么就会在根目录下寻找 index.d.ts
文件,将它视为此库的类型声明文件。
如果没有找到 index.d.ts
文件,那么就会寻找入口文件(package.json
中的 main
字段指定的入口文件)是否存在对应同名不同后缀的 .d.ts
文件。
将声明文件发布到 @types
下
如果我们是在给别人的仓库添加类型声明文件,但原作者不愿意合并 pull request
,那么就需要将声明文件发布到 @types
下。
与普通的 npm 模块不同,@types 是统一由 DefinitelyTyped 管理的。要将声明文件发布到 @types
下,就需要给 DefinitelyTyped
创建一个 pull-request
,其中包含了类型声明文件,测试代码,以及 tsconfig.json
等。
pull-request
需要符合它们的规范,并且通过测试,才能被合并,稍后就会被自动发布到 @types
下。
在 DefinitelyTyped 中创建一个新的类型声明,需要用到一些工具,DefinitelyTyped 的文档中已经有了详细的介绍.