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 扩展模块
  • /// 三斜线指令

第三方声明文件

声明文件一般不是和库文件一起发布的,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 库是指既可以通过

你可能感兴趣的:(typescript,javascript)