为TypeScript引用的JS写声明文件
写TypeScript声明文件的时候会有三个困惑,一个是声明文件是什么?一个是声明文件怎么写?还有一个是TS依据什么规则找到我们的声明文件或者说模块。
第一个问题:按照我的理解声明文件就是告诉TS编译器有哪些模块?有哪些变量?变量分别是什么类型?所以如果说原本就是TS写的代码这些都是具有的,但是JS写的代码就不会有这些,因为这些强类型是TS对JS的扩展,JS没有这个特性。
第二个问题:这个需要看官方的文档(下文也简单举例一些例子),并且要仔细看,如果语法错误也会带来很多困扰。
第三个问题:这个涉及到TS的模块解析,详情请看 官文–模块解析 ,这篇文档写的很详细,足以解释。这里列举两个我认为的重点:相对模块导入不能解析为一个外部模块声明。自己声明的模块(declare module “name”{} 这个name不能是相对的也就是说不能以 ./``````../``````/
开头),还有一个是配置文件tsconfig.json文件中{"compilerOptions": {"traceResolution": true}}
可以追踪模块解析的过程,想要深入了解这是一个好的方法,但是据我观察只能在vscode控制台中才能打印出来。
注:TS引用JS库如果JS库没有对应的声明文件编译器是不会报错的,因为没有声明文件的JS模块会隐式的获得any
类型,除非tsconfig.json中有noImplicitAny: true
这样的配置。
已经过验证上面的解释是没错的还原如下:
注:TS引用JS库如果JS库没有对应的声明文件编译器是不会报错的,因为没有声明文件的JS模块会隐式的获得any
类型,除非tsconfig.json中有noImplicitAny: true
这样的配置,指明了如有隐式转换为any类型就报错,才会报错。
TypeScript是JavaScript的超集
TypeScript会进类型检查,什么鬼?JS没有这个东西
使用TS进行开发也可以使用当前丰富的JS库,很多JS库有写好的TS声明文件,但是如果是我们自己写的JS库想要在TS中使用就需要我们自己去编写声明文件(.d.ts文件),怎么写?这就是极具个人经验主义的本文要解释的问题,如有谬误感谢指正。
本文主要是对此刻所得的整理。
下面示例基于webpack配合ts-loader,开发环境的配置可以参考我的另一篇文章基于webpack3.x从0开始搭建React开发环境。
JS库文件和对应的TS声明文件
现有的JS库可能有不同的写法,有的库导出属性,方法等,有的库导出一个类还有的库只导出一个函数。下面针对不同类型的JS库来写不同的声明文件。
声明文件也分为两种,一种是全局类型声明另一种是模块导出声明。而这两种只是声明文件的写法和JS库的写法没有关系,并不是说全局的库就需要使用全局类型声明的写法,模块的库就用模块导出的写法。
// 文件目录结构如下
-- project
|-- node_modules
|-- simple
|-- index.js
|-- lib1.js
|-- lib2.js
|-- src
|-- types.d.ts
|-- app.ts
// /node_modules/simple/index.js
// ES原生模块写法,并且导出了属性和方法
let a = 1;
function geta () {
return a;
}
function seta (val) {
a = val
}
export {geta, seta, a, a as default}
// 为simple/index.js写全局类型声明,在types.d.ts中添加如下代码
declare module "simple" {
let a: number;
export function geta(): void;
export function seta(n: number): void;
export default a;
}
// app.ts 使用三斜线指令引入声明文件
/// <reference path="./type.d.ts" />
import a, {geta, seta} from 'simple'
console.log(a)
// /node_modules/simple/lib1.js
// 导出一个类
function Ab () {
this.a = 1
}
Ab.prototype.seta = function (num) {
this.a = num
}
Ab.prototype.geta = function (num) {
return this.a
}
exports.Ab = Ab
// 在type.d.ts文件中添加
declare module "simple/lib1" {
export class Ab {
private a;
seta(n: number): void;
geta(): number
}
}
// app.ts 使用三斜线指令引入声明文件
/// <reference path="./type.d.ts" />
import a, {geta, seta} from 'simple'
import {Ab} from 'simple/lib1'
// 得以验证
console.log(new Ab())
console.log(a)
// /node_modules/simple/lib2.js
// 只导出一个函数
module.exports = function getRandom () {
return Math.random()
}
// 在type.d.ts文件中添加
declare module "simple/lib2" {
let getRandom: () => number;
export = getRandom;
}
// app.ts 使用三斜线指令引入声明文件
/// <reference path="./type.d.ts" />
import a, {geta, seta} from 'simple'
import {Ab} from 'simple/lib1'
import getRandom = require('simple/lib2')
// 得以验证
console.log(getRandom())
console.log(new Ab())
console.log(a)
上面给出的只是全局声明的写法,下面会针对上面的js库重新换成模块导出声明的写法
目录改成如下形式,app.ts文件无需做大的改动只需要将三斜线指令去除即可,一般情况下即使去除该指令types.d.ts文件还在的话TypeScript编译器还是会将该文件加载编译,这与配置有关。
并且根据我的观察发现,修改声明文件并不会马上起作用,比如在声明文件中加了一个方法,在使用的时候TypeScript编译器还是会报错说这个类型没有这个方法,需要重启webpack-dev-server(我用的是这个)
// 文件目录结构如下
-- project
|-- node_modules
|-- simple
|-- index.js
|-- index.d.ts
|-- lib1.js
|-- lib1.d.ts
|-- lib2.js
|-- lib2.d.ts
|-- src
|-- app.ts
// index.d.ts
let a: number;
export function geta(): void;
export function seta(n: number): void;
export default a;
// app.ts
import a, {geta, seta} from 'simple'
// 得以验证
console.log(geta())
//lib1.d.ts
function Ab () {
this.a = 1
}
Ab.prototype.seta = function (num) {
this.a = num
}
Ab.prototype.geta = function () {
return this.a
}
exports.Ab = Ab
import a, {geta, seta} from 'simple'
import {Ab} from 'simple/lib1'
// 得以验证
console.log(new Ab().geta())
console.log(geta())
// lib2.d.ts
let getRandom: () => number;
export = getRandom;
import a, {geta, seta} from 'simple'
import {Ab} from 'simple/lib1'
import getRandom = require('simple/lib2')
// 得以验证
console.log(getRandom())
console.log(new Ab().geta())
console.log(geta())
不想为JS类库写具体的声明文件怎么办?
在全局类型声明的文件中声明一个模块,模块什么都不做即可(这里还可以更加彻底,如文章开头的更新):
// types.d.ts 替换simple声明如下
decalre module "simple"
node_modules下的@types文件夹
默认所有可见的"@types"包会在编译过程中被包含进来。什么叫默认可见?就是说node_modules/@types
文件夹及他的子文件夹下所有的包都是可见的,还包括 ../node_modules/@types
和../../node_modules/@types
等。
有什么用呢?可以将自己的全局声明文件放在这个文件夹里面,这样就可以自动加载。
上面一段话也是错的,并不会自动的导入,所上面摘抄官网的一段话到底啥意思就不得而知了。可能意思是会被包含但是就看能不能正确解析了。
注:node_modules/@types 是TS声明文件默认位置,并且只能是全局声明的写法。
上面一句话就错了,@types里面的声明可以是模块的写法,也可以是全局写法。只要是一个模块(顶层的import/export这个文件就是一个模块或者declare “name” module { export … })就行了。
全部重写node_modules下的@types文件夹如下
在@types文件夹下的声明的包和在node_modules下的包其实一样,在node_modules没有解析到合适的.ts/.tsx/.d.ts 文件之后,TS编译器便会来到node_modules/@types文件加下寻找,如下:
import {a} from 'abc'
这种情况@/types/abc.d.ts有两种写法
export const a: number; // 只要导出即可
declare module "abc"{
export const a: number;
}
同样这个@/types/abc.d.ts的目录也可以这样:
@types/abc/index.d.ts
关于@types的配置
这个配置有两个:一个是typeRoots
表明声明文件的根文件夹,还有一个是types
表明需要包含的声明文件包(文件夹名,我试过types/efg.d.ts 这样的并不能用,需要这样types/efg/index.d.ts)。
上面划掉的部分是错的,可以用types/efg.d.ts这样的。
注验证的时候最好配合types因为上文提到过,TS编译器会默认包含所有的ts文件,所以如果不过滤,设置的typeRoots没有意义因为默认就是全部包含的。
极具个人经验部分
观察上面模块导出声明和全局类型声明两种写法发现写法差别并不大,主要区别就是声明文件放置位置不同,全局会多一个declare module "name1"
。再仔细观察会发现这个name1和import … from "name2"
中的name2是一样的,然后对于全局的声明文件还在需要的时候使用 /// <reference path = "path" />
引用进来。所以我怀疑(因为我还没有了解到是不是事实)import … from "name"
这个其实引用的是我们在声明文件中定义的module。
什么是module?
如果一个JS文件在顶层具有import或者export那么这个文件就是一个模块(模块名对应的就是文件名),在模块中定义的变量并不会暴露在全局环境下。
而上面模块导出的写法 declare module "name"{}
就相当于声明了一个模块(一个文件?)。
全局类型声明的思考
全局类型声明中只是声明了相关模块当然也可以声明其他东西,而是用全局类型声明的方法,不是import(这不是一个模块)而是三斜线指令使用 /// <reference path="path" />
这样 .d.ts 文件中的声明就被编译器读取了,之后可以再下面import … from "module"
只是这个module是我们声明出来的,并不会在对应的路径下找到相关的 .d.ts
或 .ts
或 .tsx
文件。
如果是模块导出写法必须和库在一起,否则并不知道属于哪个模块的声明,但是@types怎么解释
模块导出声明的思考
模块导出声明的写法是在 .d.ts 文件顶层是有export的所以一个文件是一个模块,如果单独引入(要使用import来引入)模块的话并不知道这个模块是哪个库的声明文件,所以需要和JS库放在一起,并且名字还要一样(后缀名不一样)。但是@types怎么解释???
关于@types的思考
见上文node_modules下的@types文件夹