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,那么可以选择发起一个PR给DefinitelyTyped。如果合并到了master上,会自动发布到npm上。即:@types/xxx。

声明文件和普通文件
*.d.ts和*.ts的区别在于:
*.d.ts对于typescript而言,是类型声明文件,且在*.d.ts文件中的顶级声明必须以declare或export修饰符开头。同时在项目编译过后,*.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比较常见的。
命名空间表示一个全局变量是一个对象,可以定义很多属性类型。同时命名空间里可能会用到一些接口类型(interface、type),这时候一般有两种写法:
- 写在
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,和es6的import语义相似,同时三斜线指令必须放在文件的最顶端。例如,当我们的声明文件过于庞大,一般都会采用三斜线指令,将我们的声明文件拆分成若干个,然后由一个入口文件引入。
npm包捆绑的声明文件语法
在配置tsconfig.json设置declaration为ture去自动生成声明文件或者是手动去写声明文件,比较常见的语法,像是:
export:导出变量export default: 默认导出export namespace:导出对象export =:commonJS导出
npm包的声明文件相对于之前的全局声明文件而言,可以理解为是局部声明文件。只有当通过import引入npm包后,才能使用对应的声明类型。而前三个语法,其实和es6类似,用法语义一目了然。
比较特殊的是,export =对应的像是import xxx = require。其实使用都是类似的,只是为了兼容AMD和commonJS才有的语法。文档

其实也就是说,对于一个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.json的types或者typings指向声明文件。在设置types或者typings后,会去找指向的声明文件。如果没有定义,则会去找根目录下的index.d.ts,再没有则去找入口文件,是否存在对应文件名的声明文件。
具体typescript如何解析查找模块类型,可以看这篇文章传送门
可以通过以下方法去让typescript引入类型:
tsconfig.json配置types指定我们的包名。- 在项目中,通过
import手动导入我们的包。 - 在项目中,通过三斜线指令引用。
参考资料
转载自:https://juejin.cn/post/6898710177969602574