前言
最近有个需求,需要写声明文件。虽然一直有在用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
手动导入我们的包。 - 在项目中,通过三斜线指令引用。