# TypeScript

提示

本节为可选内容,你可以根据需求来决定是否引入。但本人 强烈推荐 使用!TypeScript 是前端的发展趋势!成为一个合格的库作者,这项技能必不可少!而且这能为你提前 发现、规避 很多潜在的错误。

使用 TypeScript 主要有以下方式:

  1. typescript (opens new window) wiki (opens new window)
  2. @babel/preset-typescript (opens new window)

区别: [1]类型检查生成声明文件 功能 .d.ts[2] 没有类型检查,只是单纯的将 TypeScript 声明语法删除而已,也 不能生成声明文件 .d.ts,但其编译产物会更有 优势Babel 作者对提取声明文件功能的 回答 (opens new window)

既然这样,就两个都使用,各取所长。通过 @babel/preset-typescript 编译文件,通过 typescript提取声明文件,且在开发和编译期间进行 类型检查

提示

@babel/preset-typescript 是 Babel 和 TypeScript 两团队合作一年多的产物。来解决之前 .tstypescript.jsbabel.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 的演示代码删除,有兴趣也可以自己转化
1
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 的演示代码删除,有兴趣也可以自己转化
1
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 的演示代码删除,有兴趣也可以自己转化
1
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";
1
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
1
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);
    }
    // 处理完成
  }
);
1
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
1
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
1
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);
1
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,运行成功后会在 libes 目录下生成对应的 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
1
2
3
4

提示

一般直接运行 tsc --init 是不行的。使用 node_modules 里的命令有以下方式:

  1. 使用 npx,需要 npm >= 5.2。例如:npx tsc --help(推荐)
  2. 输入完整路径。例如:node_modules/.bin/tsc.cmd --help
  3. package.jsonscript 输入要执行的命令。再通过控制台 npm run <script_name> 间接运行。

提示

关于 tsconfig.json 里的配置,不清楚的可以查看 官方文档 (opens new window)

// tsconfig.json
{
  "include": ["src/**/*"], // 只对 src 目录下做检查
  "compilerOptions": {
    // ...
    "noEmit": true /* 禁止输出任何编译文件 */
    // ...
  }
}
1
2
3
4
5
6
7
8
9

最后运行 npx tsc --watch 即可。尝试修改 src/uilt.ts 下的 getRandomInt 函数的参数类型,让其类型不匹配。例如:numberstring,这样 typescript 会在终端提示哪些地方类型检查不通过。

TS类型校验错误提示示例