# TypeScript
提示
本节为可选内容,你可以根据需求来决定是否引入。但本人 强烈推荐 使用!TypeScript 是前端的发展趋势!成为一个合格的库作者,这项技能必不可少!而且这能为你提前 发现、规避 很多潜在的错误。
使用 TypeScript 主要有以下方式:
区别: [1] 有 类型检查 和 生成声明文件 功能 .d.ts
。[2] 没有类型检查,只是单纯的将 TypeScript 声明语法删除而已,也 不能生成声明文件 .d.ts
,但其编译产物会更有 优势。Babel 作者对提取声明文件功能的 回答 (opens new window)。
既然这样,就两个都使用,各取所长。通过 @babel/preset-typescript
编译文件,通过 typescript
只 提取声明文件,且在开发和编译期间进行 类型检查。
提示
@babel/preset-typescript
是 Babel 和 TypeScript 两团队合作一年多的产物。来解决之前 .ts
⇨ typescript ⇨ .js
⇨ babel ⇨ .js
复杂且长的处理链。
# 改造
为了后续内容的展开,先对 src
下的 .js
文件改造成 .ts
:
// src/util.ts
// 随机数
export function getRandomInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
// 加载图片
export async function loadImg(src: string): Promise<HTMLImageElement> {
return new Promise((resolve) => {
const img = document.createElement("img");
img.src = src;
img.onload = () => {
resolve(img);
};
});
}
export function notUse() {
console.log("全局都没有引用的代码块");
}
// 类、await、async 的演示代码删除,有兴趣也可以自己转化
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/browser.ts
const inBrowser = typeof window !== "undefined";
const UA = inBrowser && window.navigator.userAgent.toLowerCase();
const isIE = UA && /msie|trident/.test(UA);
const isEdge = UA && UA.includes("edg/");
const isChrome = UA && UA.includes("chrome") && !isEdge;
export function browser() {
return [`ie: ${isIE}`, `edge: ${isEdge}`, `chrome: ${isChrome}`].join(", ");
}
// 类、await、async 的演示代码删除,有兴趣也可以自己转化
2
3
4
5
6
7
8
9
10
11
12
// src/element.ts
import * as util from "./util";
// 使用 background 显示图片
export async function createBackgroudImg(src: string) {
const img = await util.loadImg(src);
const div = document.createElement("div");
div.style.width = `${img.width}px`;
div.style.height = `${img.height}px`;
div.style.background = `url(${src})`;
return div;
}
// 创建一个有随机数的节点
export function createRandomTextElement() {
const div = document.createElement("div");
div.innerText = `random: ${util.getRandomInt(1000, 9999)}`;
return div;
}
// 类、await、async 的演示代码删除,有兴趣也可以自己转化
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/index.ts
import { browser } from "./browser";
import { getRandomInt } from "./util";
import { createBackgroudImg, createRandomTextElement } from "./element";
export { browser, getRandomInt, createBackgroudImg, createRandomTextElement };
// 或以下
// export { browser } from "./browser";
// export { getRandomInt } from "./util";
// export { createBackgroudImg, createRandomTextElement } from "./element";
2
3
4
5
6
7
8
9
10
11
12
注意
TypeScript 不允许使用后缀名,除了引入 .js
文件或 import "file.xx"
方式直接注入。如果对这个问题有兴趣可以查看这个 issue (opens new window) 或者加入讨论。
# 整体编译
注意
如果引入模块没有具体后缀名时,Webpack 会默认按 .js
、.json
、.wasm
进行尝试匹配。所以需要在 resolve.extensions (opens new window) 重新配置后缀名解析顺序。
安装 @babel/preset-typescript
并使用,对代码进行转化:
# 安装 Babel 插件
npm install @babel/preset-typescript --save-dev
2
// build.js
var path = require("path");
var webpack = require("webpack");
webpack(
{
// 需要设置 production,自动开启代码优化功能
// 或设置以下 optimization 手动开启代码优化功能
mode: "none", // 方便查看产物的代码,可以临时设置 none
// 入口
entry: "./src/index.ts", // 修改成 ts 入口
// 输出
output: {
filename: "library.js",
path: path.resolve(__dirname, "dist"),
library: {
name: "library",
type: "umd",
},
},
target: ["web", "es5"],
// 顺序解析后缀名
resolve: { extensions: [".ts", ".js", ".json"] },
module: {
rules: [
{
test: /\.(ts)|(m?js)$/, // 多个匹配 .ts
exclude: /node_modules/,
use: {
loader: "babel-loader",
// 传入配置方式 或者 创建配置文件
options: {
presets: [
["@babel/preset-env", { debug: true, targets: "ie >= 11" }],
"@babel/preset-typescript", // 使用插件
],
plugins: [["@babel/plugin-transform-runtime", { corejs: 3 }]],
},
},
},
],
},
// optimization: {
// usedExports: true,
// minimize: true,
// concatenateModules: true,
// },
},
(err, stats) => {
if (err || stats.hasErrors()) {
// 这里处理错误
console.log(err, stats);
}
// 处理完成
}
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
运行 node build.js
,在 Chrome 和 IE 依旧可以运行 index.html
。
# 单独编译
上节是使用 Webpack 打包整体的方法,我们也需要对文件进行单独编译,类似 Gulp 单独编译 小节里的内容。
调整 compile.js
文件,Babel 配置新增 @babel/preset-typescript
插件。读取 src
下的 .ts
文件进行转化,而不是 .js
了。然后在配置中多传入一个当前编译文件的文件名 filename (opens new window),否则会 Babel 会报错。最后将输出的文件名后缀 .ts
改为 .js
。否则会出现:内容是转化后的,文件扩展名还是 .ts
的情况。
// compile.js
// 相当于 run-babel.js 的升级版
var stream = require("stream");
var { src, dest } = require("gulp");
var babel = require("@babel/core");
function compileTS(modules) {
// babel 配置
var config = {
presets: [
["@babel/preset-env", { modules, debug: true, targets: "IE >= 11" }],
"@babel/preset-typescript",
],
plugins: [["@babel/plugin-transform-runtime", { corejs: 3 }]],
};
// Commonjs 输出至 ./lib
// ES6 Module 输出至 ./es
var path = modules === false ? "./es" : "./lib";
// 读取文件
src("./src/**/*.ts")
.pipe(
// 可以使用 through2 库,会更方便
// 创建转化流,类似于双工流,但其输出是其输入的转换的转换流。
new stream.Transform({
objectMode: true,
transform: function(chunk, encoding, next) {
// 转化逻辑
babel.transform(
chunk.contents.toString(encoding), // 文件内容
// 需要额外添加 filename
{ ...config, filename: chunk.basename },
(err, res) => {
// 文件内容修改成转化后的代码
chunk.contents = Buffer.from(res.code);
// 后缀名文件 .ts -> .js
chunk.extname = ".js";
next(null, chunk);
}
);
},
})
)
.pipe(dest(path)); // 输出到某文件中
}
// 编译 TS
compileTS(false); // ES6 Module
compileTS(); // Commonjs
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
注意
如果不传额外传递文件名,Babel 会抛出以下错误: Error: [BABEL] unknown: Preset /* your preset */ requires a filename to be set when babel is called directly,babel.transform(code, { filename: 'file.ts', presets: [/* your preset */] });
。这是因为我们使用了 @babel/preset-typescript
,这个插件需要文件名参数才能正常工作,需要获取文件名的后缀名来识别是否为 .ts
文件等逻辑。
运行 node compile.js
,打开测试服务器 http://localhost:3000/
,正常运行。
# 输出声明文件
尝试: 鼠标移到 example/index.js
里从 ../lib
导入的任意方法,或者在编辑器输入该方法。
可以发现缺少类型声明,无法知道函数的入参类型和返回值类型。所以我们需要 .d.ts
文件来增强 .js
文件的类型提示。
安装 typescript
并使用,输出声明文件。依旧不通过命令运行,直接通过 Nodejs API 运行。可以参考 官方文档示例 (opens new window)。
# 安装
npm install typescript --save-dev
2
// declare.js
const ts = require("typescript");
const fs = require("fs");
const path = require("path");
// 简单实现返回文件夹下所有 .ts (除.d.ts) 文件函数
// 一般会直接使用第三方插件
function getFilesName(dirPath) {
dirPath = path.resolve(dirPath);
const collect = [];
const files = fs.readdirSync(dirPath, { withFileTypes: true });
for (const file of files) {
if (file.isDirectory()) {
const childDirPath = path.resolve(dirPath, file.name);
collect.push(...getFilesName(childDirPath));
} else {
if (file.name.match(/[^(\.d)].ts$/)) {
const filePath = path.resolve(dirPath, file.name);
collect.push(filePath);
}
}
}
return collect;
}
// 输出声明文件 .d.ts
function compileDTS(modules) {
// 路径
const output = modules === false ? "/es/" : "/lib/";
// 获取 src 下的所有需要编译声明文件的文件名
const fileNames = getFilesName("./src");
// typescript 配置
const options = {
allowJs: true,
declaration: true,
emitDeclarationOnly: true,
};
// 创建一个程序
const createdFiles = {};
const host = ts.createCompilerHost(options);
host.writeFile = (fileName, contents) => (createdFiles[fileName] = contents);
// 准备并释放 d.ts 文件
const program = ts.createProgram(fileNames, options, host);
program.emit();
// 在磁盘写入 d.ts 文件
for (const fileName in createdFiles) {
const content = createdFiles[fileName];
const buildName = fileName.replace(/\/src\//, output);
fs.writeFileSync(buildName, content);
}
}
// 执行
compileDTS();
compileDTS(false);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
运行 node declare.js
,运行成功后会在 lib
和 es
目录下生成对应的 d.ts
文件。可以将 example/index.js
库的路径改成 ../lib
或 ../es
。(因为 Webpack 的别名在 VSCode 编辑器里不能识别出具体路径)。鼠标移至对应函数或者 CTRL + 左键
,与之前对比,多了类型声明的信息。可以知道 createRandomTextElement
函数无入参,且返回值类型为 HTMLDivElement
。
# 类型检查
接下来是类型检查,这个就使用 tsconfig.json
配置文件。除非你对官方的错误信息提示你不满意,需要改造控制台的信息样式。Nodejs API 具体使用方式请参考 官方示例 (opens new window)。
先使用命令创建配置文件 tsconfig.json
,然后修改配置 include (opens new window) 和 compilerOptions.noEmit (opens new window) 配置。前面一个配置是告诉 TypeScript 只处理哪些文件或文件夹,后面一个配置是禁止输出任何编译文件,相当于只做检查。
# 如果已安装,请忽略
npm install typescript --save-dev
# 创建配置文件,执行后根目录会生成 tsconfig.json 文件
npx tsc --init
2
3
4
提示
一般直接运行 tsc --init
是不行的。使用 node_modules
里的命令有以下方式:
- 使用
npx
,需要npm >= 5.2
。例如:npx tsc --help
。(推荐) - 输入完整路径。例如:
node_modules/.bin/tsc.cmd --help
- 在
package.json
的script
输入要执行的命令。再通过控制台npm run <script_name>
间接运行。
提示
关于 tsconfig.json
里的配置,不清楚的可以查看 官方文档 (opens new window)。
// tsconfig.json
{
"include": ["src/**/*"], // 只对 src 目录下做检查
"compilerOptions": {
// ...
"noEmit": true /* 禁止输出任何编译文件 */
// ...
}
}
2
3
4
5
6
7
8
9
最后运行 npx tsc --watch
即可。尝试修改 src/uilt.ts
下的 getRandomInt
函数的参数类型,让其类型不匹配。例如:number
⇨ string
,这样 typescript
会在终端提示哪些地方类型检查不通过。