乐趣区

TypeScript-初识-声明文件

TypeScript 在开发的过程中不可避免要引用一些第三方的 JavaScript 库,虽然可以直接调用库里面的所有类和方法,但是使用过程中是缺少了类型检查、代码补全这些功能的,这时候就需要引用声明文件。

就现在使用的 VSCode 工具来说,写 JavaScript 代码时候的代码补全、接口提示这些功能,一部分是通过 VSCode 使用了正则匹配,更多的是依靠 d.ts 声明文件。

声明文件语法

  • declare var: 声明全局变量
  • declare function: 声明全局方法
  • declare class 声明全局类
  • declare enum 声明全局枚举类型
  • declare namespace 声明(含有子属性的)全局对象
  • interfacetype 声明全局类型
  • 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 声明(含有子属性的)全局对象
  • interfacetype 声明全局类型

declare var

declare var 是最简单的声明语句,就是简单地定义一个全局变量的类型。还有 declare letdeclare const,和使用 letconst 没什么区别:

// 声明文件只能定义类型,不能定义具体的实现
declare let jQuery: (selector: string) => any;
// ==========================================
jQuery('#foo');
// let 定义的变量可以被修改
jQuery = function(selector) {return document.querySelector(selector);
}

使用 declare let 定义的变量是可以被修改的,使用 declare const 就没有这样的问题。

一般引用库文件是不会去修改库文件里的变量的,修改全局变量会导致很多问题出现,所以大部分情况下都是用 declare const 而不是 declare letdeclare 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 关键字,可以直接使用 functionconstclassenum 直接声明:

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

interfacetype

declare 这个关键字可以用来定义全局的变量、方法、类、枚举、对象等,想要暴露出全局的接口、类型,不需要 declare 这个关键字,可以直接使用 interfacetype 来声明一个全局的接口或类型:

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

暴露在最外层的 interfacetype 是一个全局类型,全局类型的变量当然是越少越好,最好是将 interfacetype 放到 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 包的声明文件可能存在于两个地方:

  1. 与 npm 包绑定在一起。判断依据是 package.json 中有 types 字段,或者有一个 index.d.ts 声明文件。
  2. 发布在 @types 里。尝试安装一下对应的 @types 包就知道是否存在该声明文件。

手动写声明文件,一般有两种方案:

  1. node_modules 目录下加入 @types 文件夹,在指定包名文件夹下写 index.d.ts 声明文件。这个方案有一个风险,就是 node_modules 不稳定,不能放到 Git,也容易被删除。
  2. 在包目录下创建一个 types 目录,专门用来管理自己写的声明文件。目录结构和上面一致,这个方案需要配置 tsconfig.json 中的 pathsbaseUrl 字段。

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

只有 functionclassinterface 支持直接默认导出,其他的变量需要先定义才能默认导出:

// 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 ... requireexport =

更推荐使用 ES6 标准的 export defaultexport

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 文件。
退出移动版