0
点赞
收藏
分享

微信扫一扫

TS声明文件

now我们来看一看TS怎么声明文件,

在JS里面我们经常会使用各种第三方类库,引入方式也不太相同,常见的就是在HTML中通过script标签引入,然后就可以使用全局变量$或者jQuery了

我们通常这样获取一个 ​​id​​​ 是 ​​foo​​ 的元素:

$('#foo');
// or
jQuery('#foo');

但是TS就比较呆滞一点了,在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;

声明文件必需以 .d.ts 为后缀。

一般来说,ts 会解析项目中所有的 ​​*.ts​​​ 文件,当然也包含以 ​​.d.ts​​​ 结尾的文件。所以当我们将 ​​jQuery.d.ts​​​ 放到项目中时,其他所有 ​​*.ts​​​ 文件就都可以获得 ​​jQuery​​ 的类型定义了。

TS声明文件_全局变量

/path/to/project
├── README.md
├── src
| ├── index.ts
| └── jQuery.d.ts
└── tsconfig.json

TS声明文件_全局变量

假如仍然无法解析,那么可以检查下 tsconfig.json 中的 ​files​​include​ 和 ​exclude​ 配置,确保其包含了 ​jQuery.d.ts​文件。

第三方声明文件

当然,jQuery 的声明文件不需要我们定义了,社区已经帮我们定义好了:​​jQuery in DefinitelyTyped​​。

我们可以直接下载下来使用,但是更推荐的是使用 ​​@types​​ 统一管理第三方库的声明文件

​@types​​ 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例:

npm install @types/jquery --save-dev

书写声明文件

当一个第三方库没有提供声明文件时,我们就需要自己书写声明文件了。前面只介绍了最简单的声明文件内容,而真正书写一个声明文件并不是一件简单的事。以下会详细介绍如何书写声明文件。

在不同的场景下,声明文件的内容和使用方式会有所区别。

库的使用场景主要有以下几种:

  • 全局变量:通过 <script> 标签引入第三方库,注入全局变量
  • npm 包:通过 import foo from 'foo' 导入,符合 ES6 模块规范
  • UMD 库:既可以通过 <script> 标签引入,又可以通过 ​import​ 导入
  • 模块插件:通过 import 导入后,可以改变另一个模块的结构
  • 直接扩展全局变量:通过 <script> 标签引入后,改变一个全局变量的结构。比如为 ​String.prototype​ 新增了一个方法
  • 通过导入扩展全局变量:通过 import 导入后,可以改变一个全局变量的结构

全局变量

全局变量是最简单的一种场景,之前举的例子就是通过 ​​<script>​​​ 标签引入 jQuery,注入全局变量 ​​$​​​ 和 ​​jQuery​​。

使用全局变量的声明文件时,如果是以 npm install @types/xxx --save-dev 安装的,则不需要任何配置。如果是将声明文件直接存放于当前项目中,则建议和其他源码一起放到 ​​src​​ 目录下(或者对应的源码目录下):

TS声明文件_全局变量

/path/to/project
├── README.md
├── src
| ├── index.ts
| └── jQuery.d.ts
└── tsconfig.json

TS声明文件_全局变量

如果没有生效,可以检查下 ​​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​​​ 没有什么区别,而使用 ​​const​​ 定义时,表示此时的全局变量是一个常量,不允许再去修改它的值了:

 

TS声明文件_全局变量

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

jQuery('#foo');
// 使用 declare let 定义的 jQuery 类型,允许修改这个全局变量
jQuery = function(selector) {
return document.querySelector(selector);
}

TS声明文件_全局变量

 

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.

TS声明文件_全局变量

一般来说,全局变量都是禁止修改的常量,所以大部分情况都应该使用 const 而不是 ​var​ 或 ​let​

declare const jQuery = function(selector) {
return document.querySelector(selector)
};
// ERROR: An implementation cannot be declared in ambient contexts.

declare function

  ​​declare function​​​ 用来定义全局函数的类型。jQuery 其实就是一个函数,所以也可以用 ​​function​​ 来定义:

 

declare function jQuery(selector: string): any;

jQuery('#foo');

 

在函数类型的声明语句中,函数重载也是支持的:

TS声明文件_全局变量

declare function jQuery(selector: string): any;
declare function jQuery(domReadyCallback: () => any): any;

jQuery('#foo');
jQuery(function() {
alert('Dom Ready!');
});

TS声明文件_全局变量

declare class

当全局变量是一个类的时候,我们用 ​​declare class​​ 来定义它的类型:

 

TS声明文件_全局变量

declare class Animal {
constructor(name: string);
sayHi(): string;
}

let cat = new Animal('Tom');

TS声明文件_全局变量

 

同样的,​​declare class​​​ 语句也只能用来定义类型,不能用来定义具体的值,比如定义 ​​sayHi​​ 方法的具体实现则会报错:

TS声明文件_全局变量

declare class Animal {
constructor(name: string);
sayHi() {
return `My name is ${this.name}`;
};
// ERROR: An implementation cannot be declared in ambient contexts.
}

TS声明文件_全局变量

declare enum

使用 ​​declare enum​​ 定义的枚举类型也称作外部枚举(Ambient Enums),举例如下:

TS声明文件_全局变量

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

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

TS声明文件_全局变量

与其他全局变量的类型声明一致,​​declare enum​​ 仅用来定义类型,而不是具体的值。它仅仅会用于编译时的检查,在编译结果中会被删除。它编译结果是:

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​​ 来声明这个拥有多个子属性的全局变量。

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

jQuery.ajax('/api/get_something');

注意,在 ​​declare namespace​​​ 内部,我们直接使用 ​​function ajax​​​ 来声明函数,而不是使用 ​​declare function ajax​​​。类似的,也可以使用 ​​const​​​、​​class​​​、​​enum​​ 等语句:

TS声明文件_全局变量

declare namespace jQuery {
function ajax(url: string, settings?: any): void;
const version: number;
class Event {
blur(eventType: EventType): void
}
enum EventType {
CustomClick
}
}

jQuery.ajax('/api/get_something');
console.log(jQuery.version);
const e = new jQuery.Event();
e.blur(jQuery.EventType.CustomClick);

TS声明文件_全局变量

在编译之后,​​declare namespace​​ 内的所有内容都会被删除。它的编译结果是:

jQuery.ajax('/api/get_something');
console.log(jQuery.version);
var e = new jQuery.Event();
e.blur(jQuery.EventType.CustomClick);

嵌套的命名空间

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

TS声明文件_全局变量

declare namespace jQuery {
function ajax(url: string, settings?: any): void;
namespace fn {
function extend(object: any): void;
}
}

jQuery.ajax('/api/get_something');
jQuery.fn.extend({
check: function() {
return this.each(function() {
this.checked = true;
});
}
});

TS声明文件_全局变量

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

TS声明文件_全局变量

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

jQuery.fn.extend({
check: function() {
return this.each(function() {
this.checked = true;
});
}
});

TS声明文件_全局变量

type和interface​

除了全局变量之外,有一些类型我们可能也希望能暴露出来。在类型声明文件中,我们可以直接使用 ​​interface​​​ 或 ​​type​​ 来声明一个全局的类型:

 

TS声明文件_全局变量

// src/jQuery.d.ts

interface AjaxSettings {
method?: 'GET' | 'POST'
data?: any;
}
declare namespace jQuery {
function ajax(url: string, settings?: AjaxSettings): void;
}

TS声明文件_全局变量

 

这样的话,在其他文件中也可以使用这个接口了:

TS声明文件_全局变量

let settings: AjaxSettings = {
method: 'POST',
data: {
name: 'foo'
}
};
jQuery.ajax('/api/post_something', settings);

TS声明文件_全局变量

​type​​​ 与 ​​interface​​ 类似,不再赘述。

防止命名冲突

暴露在最外层的 ​​interface​​​ 或 ​​type​​​ 会作为全局类型作用于整个项目中,我们应该尽可能的减少全局变量或全局类型的数量。故应该将他们放到 ​​namespace​​ 下:

 

TS声明文件_全局变量

// src/jQuery.d.ts

declare namespace jQuery {
interface AjaxSettings {
method?: 'GET' | 'POST'
data?: any;
}
function ajax(url: string, settings?: AjaxSettings): void;
}

TS声明文件_全局变量

 

注意,在使用这个 ​​interface​​​ 的时候,也应该加上 ​​jQuery​​ 前缀了:

TS声明文件_全局变量

// src/index.ts

let settings: jQuery.AjaxSettings = {
method: 'POST',
data: {
name: 'foo'
}
};
jQuery.ajax('/api/post_something', settings);

TS声明文件_全局变量

声明合并

假如 jQuery 既是一个函数,可以直接被调用 ​​jQuery('#foo')​​​,又是一个对象,拥有子属性 ​​jQuery.ajax()​​(事实确实如此),则我们可以组合多个声明语句,它们会不冲突的合并起来:

TS声明文件_全局变量

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

jQuery('#foo');
jQuery.ajax('/api/get_something');

TS声明文件_全局变量

 

 

npm 包

一般我们通过 ​​import foo from 'foo'​​ 导入一个 npm 包,这是符合 ES6 模块规范的。

在我们尝试给一个 npm 包创建声明文件之前,首先​​看看它的声明文件是否已经存在​​。一般来说,npm 包的声明文件可能存在于两个地方:

  1. 与该 npm 包绑定在一起。判断依据是 package.json 中有 ​types​ 字段,或者有一个 ​index.d.ts​ 声明文件。这种模式不需要额外安装其他包,是最为推荐的,所以以后我们自己创建 npm 包的时候,最好也将声明文件与 npm 包绑定在一起。
  2. 发布到了 @types 里。只要尝试安装一下对应的包就知道是否存在,安装命令是 ​npm install @types/foo --save-dev​。这种模式一般是由于 npm 包的维护者没有提供声明文件,所以只能由其他人将声明文件发布到 ​@types​ 里了。

假如以上两种方式都没有找到对应的声明文件,那么我们就需要自己为它写声明文件了。由于是通过 ​​import​​ 语句导入的模块,所以声明文件存放的位置也有所约束,一般有两种方案:

  1. 创建一个 node_modules/@types/foo/index.d.ts 文件,存放 ​foo​ 模块的声明文件。这种方式不需要额外的配置,但是 ​node_modules​ 目录不稳定,代码也没有被保存到仓库中,无法回溯版本,有不小心被删除的风险。
  2. 创建一个 types 目录,专门用来管理自己写的声明文件,将 ​foo​ 的声明文件放到 ​types/foo/index.d.ts​ 中。这种方式需要配置下 ​tsconfig.json​ 的 ​paths​ 和 ​baseUrl​ 字段。

目录结构:

TS声明文件_全局变量

/path/to/project
├── README.md
├── src
| └── index.ts
├── types
| └── foo
| └── index.d.ts
└── tsconfig.json

TS声明文件_全局变量

​tsconfig.json​​ 内容:

TS声明文件_全局变量

{
"compilerOptions": {
"module": "commonjs",
"baseUrl": "./",
"paths": {
"*" : ["types/*"]
}
}
}

TS声明文件_全局变量

如此配置之后,通过 ​​import​​​ 导入 ​​foo​​​ 的时候,也会去 ​​types​​ 目录下寻找对应的模块的声明文件了。

注意 ​​module​​​ 配置可以有很多种选项,不同的选项会影响模块的导入导出模式。这里我们使用了 ​​commonjs​​ 这个最常用的选项,后面的教程也都默认使用的这个选项。

export

npm 包的声明文件与全局变量的声明文件有很大区别。在 npm 包的声明文件中,使用 declare 不再会声明一个全局变量,而只会在当前文件中声明一个局部变量。只有在声明文件中使用 ​export​ 导出,然后在使用方 ​import​ 导入后,才会应用到这些类型声明。

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

TS声明文件_全局变量

对应的导入和使用模块应该是这样:

TS声明文件_全局变量

// 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'
}
}

TS声明文件_全局变量

混用declare和export

我们也可以使用 ​​declare​​​ 先声明多个变量,最后再用 ​​export​​ 一次性导出。上例的声明文件可以等价的改写为:

 

TS声明文件_全局变量

// 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
}

TS声明文件_全局变量

 

注意,与全局变量的声明文件类似,interface 前是不需要 ​declare​ 的。

export namespace

TS声明文件_全局变量

// types/foo/index.d.ts

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

TS声明文件_全局变量

TS声明文件_全局变量

// src/index.ts

import { foo } from 'foo';

console.log(foo.name);
foo.bar.baz();

TS声明文件_全局变量

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;

注意,只有 function​class​ 和 ​interface​ 可以直接默认导出,其他的变量需要先定义出来,再默认导出

TS声明文件_全局变量

// types/foo/index.d.ts

export default enum Directions {
// ERROR: Expression expected.
Up,
Down,
Left,
Right
}

TS声明文件_全局变量

上例中 ​​export default enum​​​ 是错误的语法,需要先使用 ​​declare enum​​​ 定义出来,再使用 ​​export default​​ 导出:

TS声明文件_全局变量

// types/foo/index.d.ts

export default Directions;

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

TS声明文件_全局变量

如上例,针对这种默认导出,我们一般会将导出语句放在整个声明文件的最前面。

export = 

在 commonjs 规范中,我们用以下方式来导出:

 

// 整体导出
module.exports = foo;
// 单个导出
exports.bar = bar;

 

在 ts 中,针对这种导出,有多种方式可以导入,第一种方式是 ​​const ... = require​​:

// 整体导入
const foo = require('foo');
// 单个导入
const bar = require('foo').bar;

第二种方式是 ​​import ... from​​​,注意针对整体导出,需要使用 ​​import * as​​ 来导入:

// 整体导入
import * as foo from 'foo';
// 单个导入
import { bar } from 'foo';

第三种方式是 ​​import ... require​​,这也是 ts 官方推荐的方式:

// 整体导入
import foo = require('foo');
// 单个导入
import bar = require('foo').bar;

对于这种使用 commonjs 规范的库,假如要给它写类型声明文件的话,就需要使用到 ​​export =​​ 这种语法了:

TS声明文件_全局变量

// types/foo/index.d.ts

export = foo;

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

TS声明文件_全局变量

需要注意的是,上例中由于使用了 export = 之后,就不能再单个导出 ​export { bar }​ 了。所以我们通过声明合并,使用 ​​declare namespace foo​​​ 来将 ​​bar​​​ 合并到 ​​foo​​ 里。

准确地讲,​​export = ​​​不仅可以用在声明文件中,也可以用在普通的 ts 文件中。实际上,​​import ... require​​​ 和 ​​export =​​​都是 ts 为了兼容 AMD 规范和 commonjs 规范而创立的新语法,由于并不常用也不推荐使用,所以这里就不详细介绍了,感兴趣的可以看​​官方文档​​。

由于很多第三方库是 commonjs 规范的,所以声明文件也就不得不用到 ​​export =​​​ 这种语法了。但是还是需要再强调下,相比与 ​​export =​​​,我们更推荐使用 ES6 标准的 ​​export default​​​ 和 ​​export​​。

UMD库

既可以通过 ​​<script>​​​ 标签引入,又可以通过 ​​import​​​ 导入的库,称为 UMD 库。相比于 npm 包的类型声明文件,我们需要额外声明一个全局变量,为了实现这种方式,ts 提供了一个新语法 ​​export as namespace​

export as namespace

一般使用 ​​export as namespace​​​ 时,都是先有了 npm 包的声明文件,再基于它添加一条 ​​export as namespace​​ 语句,即可将声明好的一个变量声明为全局变量,举例如下:

TS声明文件_全局变量

// types/foo/index.d.ts

export as namespace foo;
export = foo;

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

TS声明文件_全局变量

当然它也可以与 ​​export default​​ 一起使用:

TS声明文件_全局变量

// types/foo/index.d.ts

export as namespace foo;
export default foo;

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

TS声明文件_全局变量

直接扩展全局变量

有的时候,我们在代码里面扩展了一个全局变量,可是它的类型却没有相应的更新过来,就会导致 ts 编译错误,此时就需要来扩展全局变量的类型。比如扩展 ​​String​​:

interface String {
prependHello(): string;
}

'foo'.prependHello();

通过声明合并,使用 interface String 即可给全局变量 ​String​ 添加属性或方法

通过导入扩展全局变量

如之前所说,对于一个 npm 包或者 UMD 库的声明文件,只有 ​​export​​​ 导出的类型声明才会有效。所以对于 npm 包或 UMD 库,如果导入此库之后会扩展全局变量,则需要使用另一种语法在声明文件中扩展全局变量的类型,那就是 ​​declare global​​。

declare global

使用 ​​declare global​​ 可以在 npm 包或者 UMD 库中扩展全局变量的类型:

TS声明文件_全局变量

// types/foo/index.d.ts

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

export default function foo(): string;

TS声明文件_全局变量

当使用方导入 ​​foo​​​ 之后,就可以使用字符串上的 ​​prependHello​​ 方法了:

// src/index.ts

import foo from 'foo';
'bar'.prependHello();




 



举报

相关推荐

0 条评论