JS迁移到TS:为第三方NPM模块(非TS开发),写一个声明文件
作者:克里斯.托马森
Apr 13, 2017
假如,你有一个由多个NPM包组成的APP,对”常规JS项目“来说,这不是一个问题。TS的最大优势就是静态类型检查,为了更好地利用这个优势,我们需要在”从第三方NPM包中引入代码“时,为这些第三方NPM包增加类型声明。
TS利用声明文件,来使编译器理解模块中的变量类型和函数签名。然而,如何为这些第三方NPM包增加声明文件的方式已经变了。因此,你从网上找到的那些方法,可能是过时的。
TS的2.2版本中,有了一个更加直接的方法来为这些流行的NPM包增加声明文件。你所要做的是:
npm install –save-dev @types/module
// for example:
npm install –save lodash
npm install –save-dev @types/lodash
npm包管理器将自动在’node_modules/@types’模块路径下,根据这些模块的子路劲创建一个叫’index.d.ts’的文件。这个文件(即’index.d.ts)内部不包含任何业务代码。这个文件仅仅是一个用来描述组件接口,比如类定义、类型用的。尽管增加了这个文件,这个模块在使用时,你还是得老老实实地import进来。
注释:其实这些第三方厂商,在了解TS的趋势后,在代码库编写了‘类型声明文件’, 默认安装的时候不下载。但是可以通过@types/module的方式去代码库主动下载。
那如果是那些没有声明文件(即该包的供应商没有编写‘类型声明模块’)的模块怎么办?
不可避免的,你的项目中总有写npm包,其供应商本没有为该包编写类型声明文件。如果要在TS文件中利用静态类型检测的好处,你就不得不在项目中自己编写这个包的声明文件。
我花了数个小时来解决这个问题。TS文档也没有跟我们说怎么样来解决这个问题,网上找到的方法很多都是旧版的解决方法。我今天写个文档,是希望有人能通过我写的东西,在面临相同问题时,能节约一点时间。
首先打开tsconfig.json这个文件,有一个叫typeRoots的属性,这个属性是用来定义”哪里去检索声明文件“。默认的,这个属性是不设置的;而且,在不设置的时候,这个属性是直接去搜索node_modules/@types下找声明文件。因为他只会在node_modules文件夹路径下找,而这个路径是用来存放包的,你的业务代码不会放在里面。
因此,第一步就是在我们的项目中,增加一个文件夹用来存储我们的声明文件。在这个项目中,我们将使用”@types“作为存放路径,当然,你想用什么名字就什么名字。
tsconfig.json
{
“compilerOptions”: {
"outDir": "./built",
"allowJs": true,
"noImplicitAny": true,
"strictNullChecks": true,
"target": "es6",
"module": "commonjs"
},
“include”: [
"./src/**/*"
],
“exclude”: [
"node_modules"
]
}
这个配置中,noImplicitAny被谁知成ture,这意味着你必须明确地增加类型声明。如果你有一个需要迁移很多包的大型项目,你应该把它关掉。
̶I̶t̶ ̶a̶l̶s̶o̶ ̶a̶d̶d̶s̶ ̶t̶y̶p̶e̶R̶o̶o̶t̶s̶:̶ ̶[̶”̶@̶t̶y̶p̶e̶s̶”̶,̶ ̶”̶.̶/̶@̶t̶y̶p̶e̶s̶”̶]̶ ̶.̶ ̶T̶h̶i̶s̶ ̶t̶e̶l̶l̶s̶ ̶t̶h̶e̶ ̶T̶y̶p̶e̶S̶c̶r̶i̶p̶t̶ ̶c̶o̶m̶p̶i̶l̶e̶r̶ ̶t̶o̶ ̶l̶o̶o̶k̶ ̶f̶o̶r̶ ̶.̶d̶.̶t̶s̶ ̶f̶i̶l̶e̶s̶ ̶i̶n̶ ̶b̶o̶t̶h̶ ̶n̶o̶d̶e̶_̶m̶o̶d̶u̶l̶e̶s̶/̶@̶t̶y̶p̶e̶s̶ ̶a̶s̶ ̶w̶e̶l̶l̶ ̶a̶s̶ ̶o̶u̶r̶ ̶c̶u̶s̶t̶o̶m̶ ̶d̶i̶r̶e̶c̶t̶o̶r̶y̶ ̶.̶/̶@̶t̶y̶p̶e̶s̶.̶ Note that all the original JavaScript source files were moved into srcto facilitate TypeScript compiling.
注意,所有的原生代码被迁移到src路径下,便于TS解释器编译。
2018-02-01更新:在最新版本的TS中,不需要在tsconfig.json中单独为typeRoots设置值。
现在,我们能够创建我们自定义的声明文件。在这个例子中,我将展示如何为一个叫做dir-obj的NPM包编写一个声明文件,这是一个我在项目中实际碰到并解决的例子。
让我们先创建一个项目
mkdir ~/dev/myproject
cd ~/dev/myproject
mkdir src
mkdir built
vim tsconfig.json
{
“compilerOptions”: {
"outDir": "./built",
"module": "commonjs",
"target": "es6",
"noImplicitAny": true,
"sourceMap": false
},
“include”: [
"src/**/*"
]
}
vim src/index.ts
<paste>
import * as dirObj from ‘dir-obj’;
const project = dirObj.readDirectory(__dirname + ‘/..’, {
fileTransform: (file: dirObj.File) => {
return file.fullpath;
}
});
console.log(JSON.stringify(project, null, 2));
</paste>
这个简单的文件,将实现”读取项目结构,并输出每个文件的全路径“
在根目录下,终端中输入一下代码,把TS文件编译成ES5文件:
tsc -p .
-p指令用来告诉tsc编译器,在当前路径寻找tsconfig.json文件
警告!无法找到模块声明文件
src/index.ts(1,25): error TS7016: Could not find a declaration file for module ‘dir-obj’. ‘/Users/chris/dev/personal/typescript-examples/node_modules/dir-obj/index.js’ implicitly has an ‘any’ type.
在当前的设置中,ts编译器不能静态检测我们的代码是否类型安全,因此,我们要增加声明文件。
mkdir src/@types
mkdir src/@types/dir-obj
vim src/@types/dir-obj/index.d.ts
这里我们在src路径下创建了@types的文件夹,以便文件在编译时被自动地引入。
我们将为dir-obj新增一个声明文件,你的声明文件必须建在npm包同名的文件夹中。即,上图中dir-obj文件为包的同名文件夹。
创建声明文件
/// <reference types=”node” />
declare module ‘dir-obj’ {
import { Stats } from “fs”;
export interface readOptions {
filter?: RegExp | Filter,
dirTransform?: DirTransform,
fileTransform?: FileTransform
}
export type Filter = (file: File) => boolean;
export type DirTransform = (file: File, value: any) => any;
export type FileTransform = (file: File) => any;
export function readDirectory(dir: string, options?: readOptions): object;
export class File {
key: string;
readonly path: string;
readonly fullpath: string;
readonly ext: string;
readonly name: string;
readonly basename: string;
constructor(dir: string, file: string);
readonly attributes: Stats;
readonly isDirectory: boolean;
readonly isRequirable: boolean;
}
}
我们在文件的开头写上”declare module ‘dir-obj’“,以明确地陈述这个声明文件所要声明的npm包。
声明文件剩下的内容中,是一系列原文件中同名的函数和类。不同的是,我们给这些同名类和函数增加了类型信息。需要指出的是,如何解读JS原包和如何写一个类型定义,不在本文档的范畴。但是,还是希望对你在正确的道路上有所帮助。
再次发送”编译项目“指令
tsc -p .
最终,再也没有报编译错误。