typescript 全局变量声明文件和模块声明文件那些事儿

typescript 全局变量声明文件和模块声明文件那些事儿

前言

最近有个需求,需要写声明文件。虽然一直有在用typescript,但是对声明文件相关信息没有怎么使用过,于是记录一下。

概念简述

声明文件

在使用第三方库的时候,想使用typescript类型检查、自动补全等等功能,需要一个描述javascript 库和模块信息的声明文件。通常来说,都是将声明语句放到一个单独文件中*.d.ts

对于第三方库,目前也是DefinitelyTyped推荐的两种方式:

  • 如果是开发者,且使用的也是typescript,那么推荐在包里捆绑自动生成的声明文件。补充:在tsconfig.json里,可以设置以下属性去自动生成声明文件:
    • declaration:设置可以自动生成*.d.ts声明文件
    • declarationDir:设置生成的*.d.ts声明文件的目录
    • declarationMap:设置生成*.d.ts.map文件(sourceMap)
    • emitDeclarationOnly:不生成js文件,仅仅生成*.d.ts*.d.ts.map
  • 如果不是开发者或者不是用typescript,那么可以选择发起一个PRDefinitelyTyped。如果合并到了master上,会自动发布到npm上。即:@types/xxx

声明文件和普通文件

*.d.ts*.ts的区别在于:

  • *.d.ts对于typescript而言,是类型声明文件,且在*.d.ts文件中的顶级声明必须以declareexport修饰符开头。同时在项目编译过后,*.d.ts文件是不会生成任何代码的。补充:默认使用tsc —init会开启skipLibCheck跳过声明文件检查,可以关闭它。
  • *.ts则没有那么多限制,任何在*.d.ts中的内容,均可以在*.ts中使用。

自动引入@types

同时根据文档,在typescript 2.0以后,默认所有可见的@types包,会在编译过程中包含进来,例如:./node_modules/@types/../node_modules/@types/../../node_modules/@types/等等。

但是,如果指定了typeRoots或者types,那么只有typeRoots目录下的包才会被引入,或者被types指定的包。

例如:设置"types": []会禁用自动引入@types包的功能。

声明文件实现

以下语法并不仅仅只能在声明文件中,只是说,相当于普通文件书写,更频繁出现在声明文件中。

declare

在声明文件中,最常看见的语法之一。用来全局声明变量、常量、类、全局对象等等,前提是该文件不是模块声明文件(后面会讲)。

declare const Jye1: string;
declare let Jye2: string;
declare class Jye3 {}
declare namespace Jye4 {}
// ...
复制代码

同时在声明函数的时候,也是支持函数重载的。

declare function name(params: string): void;
declare function name(params: number): number;
复制代码

在使用declare声明类型的时候,并不能去定义具体的实现过程。

比较特别的,像是通过declare global,可以拓展全局变量的类型和方法。

// ./types/test.d.ts
declare global {
  interface String {
    helloword(): string;
  }
}

export {};
复制代码
// ./src/test.ts
const test = "jye";
test.helloword();
复制代码

如果不加export {},会报「全局范围的扩大仅可直接嵌套在外部模块中或环境模块声明中」错误。增加export{}其实也就是为了让这个声明文件变成模块声明文件,而不是一个全局声明文件。

命名空间

前言:在typescript 1.5里,内部模块被称做「命名空间」,外部模块称为「模块」。同时module X {相当于现在推荐的写法namespace X {文档

namespace一开始的提出,主要是为了模块化(防止命名冲突等等)。但是ES6普及之后,namespace已经不再推荐使用了,更推荐使用ES6模块化。但是,在声明文件中namespace比较常见的。

命名空间表示一个全局变量是一个对象,可以定义很多属性类型。同时命名空间里可能会用到一些接口类型(interfacetype),这时候一般有两种写法:

  • 写在namespace外层,会作为全局类型被引入,从而可能污染全局类型空间。
  • 写在namespace里层,在想使用该类型的时候,可以通过namespace.interface进行使用。(推荐)
// ./types/test.d.ts
declare namespace Jye {
  interface Info {
    name: string;
    age: number;
  }

  function getAge(): number;
}
复制代码
// ./src/test.ts
let settings: Jye.Info = {
  name: "jye",
  age: 8,
};

Jye.getAge();
复制代码

同时,命名空间支持嵌套使用,即:namespace嵌套namepsace。或者简化的写法,可以写成namepsace.namespace进行声明。

同时命名空间也支持声明合并。

// ./types/test.d.ts
declare namespace Jye.Eee {
  interface Api {
    getInfo(): Info;
  }
}
复制代码

三斜线指令

三斜线指令,也是最初用来表示模块之间依赖关系。目前也是很少会去使用,不过声明文件中,还是有很多会去使用。

在三斜线指令的语法中,目前可能会去比较常用的两种语法:

  • /// <reference path="./lib/index.d.ts" />:表示对一个文件的依赖。
  • /// <reference types="jye" />:表示对一个库的依赖。

说白了,三斜线的path & types,和es6import语义相似,同时三斜线指令必须放在文件的最顶端。例如,当我们的声明文件过于庞大,一般都会采用三斜线指令,将我们的声明文件拆分成若干个,然后由一个入口文件引入。

npm包捆绑的声明文件语法

在配置tsconfig.json设置declarationture去自动生成声明文件或者是手动去写声明文件,比较常见的语法,像是:

  • export:导出变量
  • export default: 默认导出
  • export namespace:导出对象
  • export =:commonJS导出

npm包的声明文件相对于之前的全局声明文件而言,可以理解为是局部声明文件。只有当通过import引入npm包后,才能使用对应的声明类型。而前三个语法,其实和es6类似,用法语义一目了然。

比较特殊的是,export =对应的像是import xxx = require。其实使用都是类似的,只是为了兼容AMDcommonJS才有的语法。文档

其实也就是说,对于一个npm包的声明文件,只有通过export导出的类型,才能被使用。

全局声明和局部声明

其实写到这里,前面有两点没有说清楚,什么是全局声明,什么是局部声明。

我的理解是,如果这个声明文件被typescript引入了,那么这个文件不包含import export,那么这个文件中包含的declare & interface & type就会变成全局声明。反之,若是这个文件包含了import export,那么这个文件包含的declare & interface & type则会是局部声明,不会影响到全局声明。

@types/react为例:配置tsconfig.json关闭自动引入@types文件,且在@types/react中增加declare

// @types/react/index.d.ts
export = React;
export as namespace React;

declare namespace Jye {
  interface Info {
    name: string;
    age: number;
  }

  function getAge(): number;
}
复制代码

同时在a文件import React from 'react,在b文件使用相关类型

// src/b.ts
React.Children;  // ok

let settings: Jye.Info = {  // 找不到命名空间“Jye”。ts(2503)
  name: 'jye',
  age: 8,
};

Jye.getAge(); // 找不到命名空间“Jye”。ts(2503)
复制代码

可以看到,在项目中引入了react后,那么该文件导出的类型则被引入到全局中。但是除却export出来的类型,其他declare的类型,则无法被使用。

同理,可以在项目中,定义*d.ts,通过设置export {}将其从一个全局声明文件变成一个模块声明文件。那么对应declare内容则会无法使用,只能通过引入文件后,使用其export出来的类型。

那么总结:如果没有export import,那么这个文件被引入后,则会是一个全局声明,(也就是说这个文件是全局声明文件)。否则,这个文件被引入后,仅仅其export的内容,被引入到全局里,其他内容则作为局部声明(这个文件是模块声明文件)。

类型引用

在项目中,设置package.jsontypes或者typings指向声明文件。在设置types或者typings后,会去找指向的声明文件。如果没有定义,则会去找根目录下的index.d.ts,再没有则去找入口文件,是否存在对应文件名的声明文件。

具体typescript如何解析查找模块类型,可以看这篇文章传送门

可以通过以下方法去让typescript引入类型:

  • tsconfig.json配置types指定我们的包名。
  • 在项目中,通过import手动导入我们的包。
  • 在项目中,通过三斜线指令引用。

参考资料

www.typescriptlang.org/

jkchao.github.io/typescript-…

ts.xcatliu.com/

分类:
前端
标签: