配置名称 含义
module 设置编译结果中使用的模块化标准
moduleResolution 设置解析模块的模式
noImplicitUseStrict 编译结果中不包含”use strict”
removeComments 编译结果移除注释
noEmitOnError 错误时不生成编译结果
esModuleInterop 启用 es 模块化交互非 es 模块导出

本章节开始介绍 ts 的模块化,在介绍模块的过程中,会涉及到一些模块化的相关配置,相关配置字段都罗列在这张表中了。

5-1. 在 TS 中使用模块化

前言

任务列表

  • 掌握 ts 中如何书写模块化语句;

notes

前端领域的模块化标准

  • es6
  • commonjs
  • amd
  • cmd
  • umd
  • system
  • esnext

Best Practice
TS 中,导入和导出模块,推荐统一使用 es6 的模块化标准。

:::tips 这里涉及到一些历史问题

ts 出现的时间点是在 es6 发布的时间(15年)之前的,那时候 es 官方还没有推出自己的模块化标准。在 15 年之前,可以理解为是模块化标准群魔乱舞的时代,那时候 ts 也整出了自己的模块化标准,不过在 es6 module 标准出来之后。ts 立刻就支持了 es6 的模块化标准,ts 自己的那套标准,已经淹没在历史长河中了。

现阶段,我们在 ts 中书写模块化语句,只要关注 es6 module 即可。 :::

  1. {
  2. "compilerOptions": {
  3. "target": "es6", // 配置我们写的 ts 的模块化规范
  4. "module": "es6", // 配置编译结果 js 的模块化规范
  5. },
  6. }

codes

  1. export const nickname = "dahuyou";
  2. export function sum(a: number, b: number) {
  3. return a + b;
  4. }
import { nickname, sum } from "./myModule";

console.log(nickname); // => dahuyou

console.log(sum(1, 2)); // => 3

和我们之前写 es6 module 时,完全一样,直接写就完事。

但是,也有一些需要注意的细节问题:

  1. 智能提示问题
  2. 后缀名问题

智能提示问题

image.png

当我们输入 nick 时,它就会弹出智能提示,我们只需要按下回车,即可将其导入:

image.png

之所以会有这么有好的智能提示,是因为 TS 拥有完整的类型检查,当我们去访问一个当前模块中不存在的玩意儿的时候,ide 就会帮我们去各个模块中查找,这样对于我们开发效率的提升,是非常有帮助的。

问:为啥在记笔记测试的时候木有智能提示? 答:有可能是因为层次结构太乱了。

:::warning 如果智能提示一切正常,直接跳过以下内容。 :::

👇🏻 记录的内容是描述一个记笔记时的怪异现象

环境:该问题是当时是在 windows 系统上出现的,在 mac 上时不存在。

image.png

尝试只打开 codes 目录,如下图所示,这样就会有智能提示了。

image.png

自动从带有基本导出 nickname 的模块中导入 nickname

image.png

以上介绍的所有智能提示,都有一个前提:使用的是具名导出(也叫基本导出)。

如果使用匿名导出,那么是没有那么完善的智能提示的。

道理很简单,具名导出是有名字的,有名字才能找;匿名导出是没有名字的,没名字了咋找呢。

具名导出和匿名导出

  • 具名导出:又叫基本导出,比如 export const nickname = 'dahuyou';
  • 匿名导出:又叫默认导出,比如 export default 'dahuyou';

小结:
若想要享用智能提示的效果,那么我们应该使用基本导出的形式。若我们使用的是默认导出的形式,那么 ts 是不知道我们导出的东西是什么名字的,它就没法给我们提供智能提示。

后缀名问题

我们在导入文件的时候,不要加 .ts 后缀名。

image.png

Q & A

🤔 ts 中使用 es6 module 导出内容,为什么推荐使用具名导出? 为了享受 ide 为我们提供的更加友好的智能提示

🤔 为什么在导入模块的时候,不能带上 .ts 后缀名呢? image.png

道理很简单,这些内容在编译后,是不会发生变化的,而编译后的编译结果中,都是 js 文件。

比如:import { name, sum } from “./myModule.ts”
在编译之后,它还是:import { name, sum } from “./myModule.ts”

但是编译后都是 js 文件,myModule.ts 已经变为了 muModule.js,这时候编辑结果就没法正常运行了。

image.png

PS:加上 .ts 后缀确实是报错了的,但是依旧可以完成编译。(虽然不知道原因)

5-2. 编译结果中的模块化

前言

任务列表

  • 理解编译结果
  • tsc --watch
  • noImplicitUseStrict 编译结果中不包含"use strict"
  • removeComments 编译结果移除注释

难点在于:理解「使用 es6 模块化规范所写的模块,转换为等效的 commonjs 规范」的代码。

参考资料 原文链接:
cubox 个人链接:

因为在编译结果中,出现了 void 0,由于不理解,就上网找了篇相关文章查阅。这篇文章已经读完,感觉写得非常棒,把 void 操作符的用法讲解的明明白白,忘记了的话,可以会看一下这篇文章。


原文链接:

个人链接:

编译结果中还出现了这种写法 —— (0, function)(param)
一句话总结:这种写法令函数体中的 this 指向全局对象。
如果不理解的话,可以参考一下上述文章。

版本问题:由于版本不同的原因,最新 v4.7 版本,生成的编译结果和袁进老师当时授课时使用的版本有所不同。不过大体上是都一致的,重点在于能够理解即可。

notes

target 和 module

目标文件和编译结果中的模块化标准,都是可以配置的。

{
  "compilerOptions": {
    "target": "xxx", // => 配置我们在 ts 文件中写的模块化标准
    "module": "xxx", // => 配置编译结果的模块化标准
  },
}

tsc --watch

使用该命令,用于监听 ts 文件的变化,一旦文件内容发生了变化,那么就会重新编译。(只编译,不运行)

目标文件和编译结果都是 es6

{
  "compilerOptions": {
    "target": "es6",
    "module": "es6",
    "lib": [
      "es2016"
    ],
    "outDir": "./dist",
    "strictNullChecks": true
  },
  "include": [
    "./src"
  ],
}

先给出结论:当 target 和 module 配置的都是 es6 module 标准时,生成的编译结果和我们写的 ts 文件,模块化部分是保持不变的。

目标文件

export const nickname = "dahuyou";
export function sum(a: number, b: number) {
  return a + b;
}
import { nickname, sum } from "./myModule";
console.log(nickname); // => dahuyou
console.log(sum(1, 2)); // => 3

编译结果

export const nickname = "dahuyou";
export function sum(a, b) { // 少了类型约束信息
  return a + b;
}
import { nickname, sum } from "./myModule";
console.log(nickname); // => dahuyou
console.log(sum(1, 2)); // => 3

removeComments

从上面的编译结果中可以看出,我们在 ts 中编写的相关注释,也是会生成到编译结果中的。若我们想要去掉注释,只需要加上一个配置 removeComments,将其值设置为 true 即可。

{
  "compilerOptions": {
    "removeComments": true
  },
}

目标文件 - es6 | 编译结果 - commonjs

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "lib": [
      "es2016"
    ],
    "outDir": "./dist",
    "strictNullChecks": true,
    "removeComments": true
  },
  "include": [
    "./src"
  ],
}

目标文件

import sayHello, { nickname, sum } from "./myModule";
console.log(nickname); // => dahuyou
console.log(sum(1, 2)); // => 3
sayHello(); // => hello world.
export const nickname = "dahuyou";
export function sum(a: number, b: number) {
  return a + b;
}
export default () => {
  console.log("hello world.");
};

编译结果

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const myModule_1 = require("./myModule");
console.log(myModule_1.nickname);
console.log((0, myModule_1.sum)(1, 2));
(0, myModule_1.default)();
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sum = exports.nickname = void 0;
exports.nickname = "dahuyou";
function sum(a, b) {
    return a + b;
}
exports.sum = sum;
exports.default = () => {
  console.log("hello world.");
};
  • **"use strict";**

表示开启严格模式 :::tips 其实没必要开启严格模式,我们写的 ts 代码比严格模式的约束还要强很多,所以编译结果中的这个严格模式是可以直接去掉的。
通过 noImplicitUseStrict 配置可选择是否在编译结果中包含 "use strict";这条语句。 :::

  • **Object.defineProperty(exports, "__esModule", { value: true });**

等效:exports.__esModule = true :::tips 该语句的作用,暂时不用管,之后会介绍。 :::

  • **exports.sum = exports.nickname = void 0;**

等效:exports.sum = undefined; exports.nickname = undefined;
这种做法就相当于我们在模块中声明了两个变量 sumnickname。但是还没有给变量进行赋值。 :::tips void 操作符还有其它用法,详情可以查看参考资料。 :::

  • **(0, myModule_1.sum)(1, 2)****(0, myModule_1.default)();**

这么写的目的,是为了绑定方法中的 this,令 this 指向全局而非 myModule。

不要误以为上述写法等效于:myModule_1.sum(1, 2)myModule_1.default()

因为这两种方式调用函数,函数中的 this 指向是不一样的。

  1. myModule_1.sum(1, 2)这种方式,函数中的 this 将指向 myModule 对象
  2. (0, myModule_1.sum)(1, 2)这种方式,函数中的 this 将指向全局对象

而我们在编译之前所写的 sum 方法,它里边如果用到了 this,那么 this 就是指向全局的。第二种写法的作用就是为了确保方法中的 this 指向依旧指向全局。

  • **exports.default = () => { console.log("hello world."); }**

es6 的默认导出,转为 commonjs 后,会转为 exports 的 default 属性。

  • **const myModule_1 = require("./myModule");**

简单说明一下这个 1,它表示当前模块 ./myModule是第一次被使用。如果我们将导入语句改为下面这种写法:

import sayHello from "./myModule";
import { nickname, sum } from "./myModule";
console.log(nickname); // => dahuyou
console.log(sum(1, 2)); // => 3
sayHello(); // => hello wo
Object.defineProperty(exports, "__esModule", { value: true });
const myModule_1 = require("./myModule");
const myModule_2 = require("./myModule");
console.log(myModule_2.nickname);
console.log((0, myModule_2.sum)(1, 2));
(0, myModule_1.default)();

会发现第二次使用 ./myModule 模块时,后边的编号会自动自增。

noImplicitUseStrict

若想去掉编译结果中生成的 "use strict",那么需要配置 noImplicitUseStrict

{
  "compilerOptions": {
    "noImplicitUseStrict": true // 去掉严格模式
  },
}

Q & A

🤔 编译结果中的模块化标准,是否是可配置的? 是

可以通过 tsconfig.json 中的 module 字段来配置;

编译结果中的模块化标准,可以配置为 es6 module、commonjs 等等,但是最常用的就是这两个,并且我们也只需要掌握好这两个即可;

🤔 tsc —watch 这个命令有什么用? 当我们编写的 ts 文件发生变化时,它会帮我们自动进行编译,生成对应的 js 文件;

并不会自动执行编译结果文件

🤔 默认情况下,编译结果是否是严格模式? 是

配置 compilerOptions.noImplicitUseStrict: true 去掉编译结果中的严格模式

🤔 默认情况下,ts 中的注释,是否会生成到编译结果中? 会

配置 compilerOptions.removeComments: true 移除编译结果中的注释

🤔 默认情况下,编译目标 ts 文件发生错误,是否可以正常编译? 可以

通过配置 compilerOptions.noEmitOnError,可以设置编译目标发生错误时,无法生成编译结果。

5-3. 解决默认导入的错误

前言

image.png

使用 es6 module,还有一些细节上的问题待处理,比如我们想要访问 node 中的一个内置模块 fs,使用这种 import 的方式来导入,是不可行的。该问题如何解决,就是本节课所要介绍的重点。

任务列表

  • compilerOptions.noEmitOnError
  • compilerOptions.esModuleInterop

参考资料

notes

错误原因分析

在目标文件中,尝试导入 node 环境下的 fs 模块。

import fs from "fs";
fs.readFileSync("./");
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = require("fs");
fs_1.default.readFileSync("./");
  • fs_1.default.readFileSync("./")

通过编译结果,我们可以非常清楚地看出,为何这种写法会出现问题。

我们在 ts 文件中写的 fs.xxx 代码,被编译后会转换为 fs_1.default.xxx。这就是为什么我们不能使用上面这种写法来导入 fs 的原因。

正确的编译结果应该是 fs_1.readFileSync

出现这种现象的本质原因在于:node 的内置 fs 模块,它并非使用 es6 module 规范来写的,它压根就没有 export default 这一说。

image.png

cmd + f 全局搜一下 module.exports,会发现 fs 模块实际上采用的是 commonjs 规范来导出的。

ts 中为了处理 commonjs 和 es6 两者之间的交互,对于默认导出采用 default 属性来表示,这是我们上一节课中介绍过的内容。所以对于我们这种传统的 es6 module 导入默认导出内容的写法,默认是会报错的。

:::warning 充分理解发生错误的原因,对于后续内容的理解非常重要。 :::

细节:导入的模块未使用
若我们在 ts 中导入了一个模块,但是没有使用该模块,那么该模块的导入语句并不会生成到编译结果中。比如上述代码,若我们只是导入了 fs 模块,但是没有使用该模块,那么导入语句就不会生成到编译结果中。

方案1:使用具名导入

import { readFileSync } from "fs";
readFileSync("./");
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = require("fs");
(0, fs_1.readFileSync)("./");

导入具名导出**import { readFileSync } from "fs"**
虽然使用默认导出不行,但是我们可以使用基本导出(具名导出),这样导入的内容,编译结果就是正常的。

新的问题
fs 中的具名导出那么多,我们不知道具体有哪些具名导出;而且每要使用一个 fs 模块提供的 api,就到在 import 中写上该 api,操作上显得有点繁琐。

方案2:import * as fs from "fs"

若我们希望将 fs 模块导出的所有内容,丢到一个变量中,然后我们通过 变量.xxx 的形式来访问指定的 api,使用上面这种写法就可以了。

import * as fs from "fs";
fs.readFileSync("./");
Object.defineProperty(exports, "__esModule", { value: true });
const fs = require("fs");
fs.readFileSync("./");

这种方案也是 ts 官方建议我们这么做的

image.png

会发现,只要我们将鼠标的悬停在 fs 上,vscode ide 就会展示出该模块的一些描述信息,在描述信息中,我们也可以看到正确的导入方式应该如何书写。

也可以按下 cmd 键,加鼠标左键,快速定位到 fs 模块的类型声明文件中查看相关描述信息。

image.png

方案3:开启 esModuleInterop

文档:链接
默认值:false
含义:是否开启 es module 和 commonjs 模块化标准之间的交互。

import fs from "fs"; // => ×
import { readFileSync } from "fs"; // => √
import * as fs from "fs"; // => √

虽然前面介绍的两种解决方式,都可以解决我们提到的错误式写法所导致的问题。但是,我们在写代码的时候,还是很有可能会写出错误写法那样的代码。对于该问题,我们加上 esModuleInterop 配置来解决。

下面介绍的内容,就是给强迫症患者,非得使用 import fs from "fs"; 这种写法来写的。

{
  "compilerOptions": {
    "esModuleInterop": true,
  },
}

在加上该配置过后,我们再来看看原来错误写法的代码:

import fs from "fs";
fs.readFileSync("./");
var __importDefault = (this && this.__importDefault) || function (mod) {
  return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
fs_1.default.readFileSync("./");

image.png

👇🏻 简单来分析一下编译后的结果,看看 ts 是如何帮我们处理的。

首先先看看结论:__importDefault 是一个辅助函数,该函数的返回值要么是 require("fs") 要么是 { default: require("fs") },由于现在是不会报错的,所以返回的必然是后者。其实就是帮我们包裹了一层,将 __importDefault(require("fs")) 转为了 { "default": require("fs") }

也就是说,下面这两条语句其实是等效的:

  1. const fs_1 = __importDefault(require("fs"))
  2. const fs_1 = { "default": require("fs") }

当我们再去访问:fs_1.default.readFileSync("./") 这个时,实际上就是在访问 require("fs").readFileSync("./")

其实为什么是后者 { "default": mod } 也不难分析:

  1. (this && this.__importDefault)
    • 先看全局对象是否存在,如果存在,那么在看全局对象上边是否已经有了辅助函数 __importDefault
      如果已经存在了 __importDefault 那么就没必要再将后续的函数表达式赋值给 __importDefault
    • 如果当前模块中还没有创建辅助函数 __importDefault 那么就初始化一个辅助函数
  2. (mod && mod.__esModule) ? mod : { "default": mod }
    • mod 参数,表示的就是我们导入的模块
    • 先确保我们导入的模块是存在的,如果不存在,那么直接将 { "default": mod } 给返回
    • 再确保当前模块是否是 es 模块,如果不是,那么直接将 { "default": mod } 给返回
    • 如果当前导入的模块是存在的,并且是使用 es 规范来写的,那么直接将 mod 给返回

显然,我们这里所要导入的 fs 模块,它是使用 commonjs 规范来写的,并非一个 es 模块,所以最终返回的会是 { "default": mod }

noEmitOnError

错误时不生成编译结果。

let str: string = 123; // => ×

虽然上述代码在 ts 中是会报错的。但是,类型约束并不会生成到 js 中,所以在 js 中,是不会报错的。默认情况下,依旧可以对该 ts 文件进行编译,编译生成的 js 文件的内容如下:

let str = 123;

若我们想让 ts 在报错的情况下,无法进行编译,那么需要配置 onEmitOnError 字段:

{
  "compilerOptions": {
    "noEmitOnError": true,
  },
}

5-4. 如何在 TS 中书写 commonjs 模块化代码

前言

任务列表

  • 掌握如何在 ts 中书写 commonjs 模块化代码
    • 导出:export = xxx
    • 导入:import xxx = require("xxx")

notes

在 ts 中编写模块化代码,最好就直接使用 es module 规范来写,前面已经介绍过相关细节。

若非要使用 commonjs 规范来写,也不是不行,本节将介绍如何在 ts 中书写 commonjs 规范的模块化代码。

通过本节介绍的内容,我们将会认识到:为什么推荐在 ts 中推荐直接使用 es module 规范来写模块化代码;在 ts 中书写 commonjs 模块化代码都有哪些问题。

无法获取智能提示

因为 ts 是 js 的超级,所以我们完全按照传统 commonjs 的书写规范来写,是 OK 的。

module.exports = {
  name: "dahuyou",
  sum(a: number, b: number) {
    return a + b;
  }
}
const myModule = require("./myModule");

编译结果

module.exports = {
    name: "dahuyou",
    sum(a, b) {
        return a + b;
    }
}
const myModule = require("./myModule");

虽然这种写法,程序也能正常运行,但是它有一个缺点:我们将无法获取到类型检查。

image.png

我们导入的内容会被识别为 any 类型,自然也就没法获取到智能提示。

获取智能提示

若想获取到类型检查,我们不应该采用这种方式来写。下面介绍若想要获取类型提示,应该如何书写。

export = {
  name: "dahuyou",
  sum(a: number, b: number) {
    return a + b;
  }
}
import myModule from "./myModule"; // 写法1
// import myModule = require("./myModule"); // 写法2

导出:将原来的 module.exports = xxx 改为 export = xxx
导入:将原来的 const myModule = require("./myModule") 改为 import myModule from "./myModule" 或者 import myModule = require("./myModule")

:::warning 这么写的前提:配置 compilerOptions.esModuleInteroptrue :::

编译结果

module.exports = {
    name: "dahuyou",
  sum(a, b) {
    return a + b;
  }
}
Object.defineProperty(exports, "__esModule", { value: true });

编译结果几乎是完全一致的,虽然 1.js 的编译结果有所不同,但是和之前的编译结果相比,功能都是一样的。

对比上面那种安全按照 commonjs 规范来写,这种写法的优势在于可以得到类型检查,提供更加友好的智能提示。

image.png

小结

导出的写法:
image.pngimage.png

导入的写法:
image.png

Q & A

🤔 为什么推荐我们在 ts 中使用 es module 来写? 在 ts 中编写模块化代码,推荐直接使用 es module 规范来写,下面是几点原因:

  1. es module 是官方推出的,commonjs 更有可能会推出历史舞台

es module 是官方推出的模块化规范
commonjs 是社区规范

除了 es modulecommonjs 之外,还有很多其他的模块化规范。

但是随着时间推移,规范应该会逐步统一,大多规范都将成为历史,直接使用官方推出的 es module 规范来写模块化语句即可。

  1. 在 ts 中书写 commonjs 模块化代码很别扭

如果完全按照 commonjs 规范来写,那么 ts 没法给我们提供类型检查和智能提示;

如果想要智能提示,那么我们写的模块化代码感觉是 es module 和 commonjs 的杂交品;

5-5. 模块解析

前言

任务列表

  • compilerOptions.moduleResolution 配置 ts 中采用的模块解析策略

参考资料

查阅 compilerOptions.moduleResolution 该配置的相关描述

notes

{
  "compilerOptions": {
    "moduleResolution": "node",
    // 配置模块解析策略为 node
    // 其实默认值就是 node
  }
}

模块解析是什么

模块解析:应该从什么位置查找模块

在 TS 中,有两种模块解析策略:

  1. classic:经典模块解析策略
  2. node:node 解析策略

classic 解析策略

classic 这种解析策略,在 es6(2015年)出现之前就已经有了,它是 TS 自身特有的模块解析策略。

但是,classic 经典模块解析策略在目前已经过时了,如果我们有幸接触到一些七、八年前用 ts 写的项目,那才有可能与之相遇。

所以呢,它的规则,这里就不赘述了。

node 解析策略

node 解析策略和 node 环境下的模块化细节几乎完全相同,唯一的变化就是将 js 替换为 ts,以下是对 node 解析策略的简单说明:

  • 相对路径require("./xxx")
    1. 先看当前目录下的 xxx 文件;
    2. 再将其视作一个目录,尝试去找目录下的 package.json 文件下的 main 字段;
    3. 不断往上找;
  • 非相对路径require("xxx")
    1. 先看当前目录下的 node_modules 目录;
    2. 不断往上找;


:::tips 详情: 1-3. Node的模块化细节

之后该链接可能会失效,会将其合并到 1. Node 核心 中,如果失效了,请前往知识库 node 查看

PS:对于如何查找模块,这个基本上不需要担心,很少会出现引入错误模块的情况。所以查找模块的细节规则,这个就非常矛盾,袁进老师在 node 的教程中,把这一块介绍地非常详细,但是引入错误模块的可能性实在太小。诶,当时也记录地非常详细,怎么说呢,这一块。有精力就看看吧,没精力就跳过好了,这一块的细节太多,现在也基本忘记了。 :::

5-6. 练习:使用模块化优化扑克牌程序

前言

任务列表

  • 基于4. 扩展类型-枚举(4-3 的练习)独立完成本节练习
  • 重置编译结果目录
    • rm -rf dist && tsc - mac
    • rd /s /q xxx & tsc - windows
  • 理解类型声明模块(types.ts),在编译时将被忽略

参考资料


notes

在 windows 和 mac 下,该如何通过命令行的方式来删除一个目录,并在删除目录之后,立即执行其它命令?

  • **rm -rf dist && tsc**

若是 mac 用户,使用该命令重置编译结果目录 dist

  • **rd /s /q xxx & tsc**

若是 windows 用户,使用该命令重置编译结果目录 dist

:::tips rd,表示 remove directory 删除目录 :::

image.png

codes

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "lib": [
      "es2016"
    ],
    "outDir": "./dist",
    "strictNullChecks": true
  },
  "include": [
    "./src"
  ],
}
import {createDeck, printDeck} from "./funcs"

const deck = createDeck();

printDeck(deck);
// 黑桃(Spade)、红桃(Heart)、方块(Diamond)、梅花(Club)
export enum Colors { // 花色
  Heart = "❤",
  Spade = "♠",
  Club = "♣",
  Diamond = "◇",
}

// one,two,three,four,five,six,seven,eight,nine,ten,eleven,twelve,thirteen
// 牌编号
export enum Nums {
  one = "A",
  two = "2",
  three = "3",
  four = "4",
  five = "5",
  six = "6",
  seven = "7",
  eight = "8",
  nine = "9",
  ten = "10",
  eleven = "J",
  twelve = "Q",
  thirteen = "K",
}
import { Colors, Nums } from "./enums";

export type Poker = {
  // 一张牌
  number: Nums | "JOKER" | "joker";
  color?: Colors;
};

export type Deck = Poker[]; // 一副牌
import { Colors, Nums } from "./enums";
import { Deck } from "./types";

// 创建一副牌
export function createDeck() {
  const deck: Deck = [];
  // 插入大小王
  deck.push({
    number: "joker",
  });
  // 插入大王
  deck.push({
    number: "JOKER",
  });
  // 插入 A ~ K
  const colorKeys = Object.keys(Colors);
  const numKeys = Object.keys(Nums);
  for (let i = 0; i < colorKeys.length; i++) {
    for (let j = 0; j < numKeys.length; j++) {
      deck.push({
        number: Nums[numKeys[j]],
        color: Colors[colorKeys[i]]
      });
    }
  }
  deck.sort(() => Math.random() - 0.5); // 打乱牌序
  return deck;
}

// 打印一副扑克
export function printDeck(deck: Deck) {
  let result = "底牌:";
  const desk = deck.splice(0, 3); // 底牌
  desk.forEach((poker, i) => {
    result += poker.color ? ` ${poker.color}${poker.number}` : ` ${poker.number}`;
  });
  result += '\n用户1:'
  deck.forEach((poker, i) => {
    if(i % 17 === 0 && i !== 0) result += `\n用户${i / 17 + 1}:`;
    result += poker.color ? ` ${poker.color}${poker.number}` : ` ${poker.number}`;
  });
  console.log(result);
}
{
  "devDependencies": {
    "@types/node": "^17.0.22"
  },
  "scripts": {
    "dev": "nodemon --watch src -e ts --exec ts-node ./src/index.ts",
    "build": "rm -rf dist && tsc"
  }
}

场景描述:
当我们执行 tsc 命令时,dist 目录中已存在一些 .js 文件,比如 1.js
若此时直接执行 tsc 命令对我们所写的目标文件 .ts 文件进行编译,那么 1.ts 默认会依旧存在,并不会被覆盖

如果我们想要每次执行 tsc 命令之前,都先将存放编译结果文件的目录 dist 目录给提前清空,那么该如何实现呢?

  • 如果是 windows 用户:rd /s /q dist & tsc
  • 如果是 mac 用户:rm -rf dist && tsc

执行 npm run dev:在内存中进行编译并运行
image.png

编译结果:对于编译结果,这里做一点说明,那就是对类型别名的处理。

import { Colors, Nums } from "./enums";

export type Poker = {
  // 一张牌
  number: Nums | "JOKER" | "joker";
  color?: Colors;
};

export type Deck = Poker[]; // 一副牌

由 types.ts 中的内容不难看出,这里面导出的内容都是类型别名

前面介绍过,类型别名并不会生成到编译结果中,所以对于 types.ts 模块,ts 会进行特殊处理

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

相当于编译结果仅仅是生成了一个空模块。

若原来的编译目标文件中,有文件引用了该模块,那么在编译结果中,引用代码会被直接忽略。

import { Colors, Nums } from "./enums";
import { Deck } from "./types";

// 创建一副牌
export function createDeck() {
  const deck: Deck = [];
  // 插入大小王
  deck.push({
    number: "joker",
  });
  // 插入大王
  deck.push({
    number: "JOKER",
  });
  // 插入 A ~ K
  const colorKeys = Object.keys(Colors);
  const numKeys = Object.keys(Nums);
  for (let i = 0; i < colorKeys.length; i++) {
    for (let j = 0; j < numKeys.length; j++) {
      deck.push({
        number: Nums[numKeys[j]],
        color: Colors[colorKeys[i]]
      });
    }
  }
  deck.sort(() => Math.random() - 0.5); // 打乱牌序
  return deck;
}

// 打印一副扑克
export function printDeck(deck: Deck) {
  let result = "底牌:";
  const desk = deck.splice(0, 3); // 底牌
  desk.forEach((poker, i) => {
    result += poker.color ? ` ${poker.color}${poker.number}` : ` ${poker.number}`;
  });
  result += '\n用户1:'
  deck.forEach((poker, i) => {
    if(i % 17 === 0 && i !== 0) result += `\n用户${i / 17 + 1}:`;
    result += poker.color ? ` ${poker.color}${poker.number}` : ` ${poker.number}`;
  });
  console.log(result);
}
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.printDeck = exports.createDeck = void 0;
const enums_1 = require("./enums");
// 创建一副牌
function createDeck() {
    const deck = [];
    // 插入大小王
    deck.push({
        number: "joker",
    });
    // 插入大王
    deck.push({
        number: "JOKER",
    });
    // 插入 A ~ K
    const colorKeys = Object.keys(enums_1.Colors);
    const numKeys = Object.keys(enums_1.Nums);
    for (let i = 0; i < colorKeys.length; i++) {
        for (let j = 0; j < numKeys.length; j++) {
            deck.push({
                number: enums_1.Nums[numKeys[j]],
                color: enums_1.Colors[colorKeys[i]]
            });
        }
    }
    deck.sort(() => Math.random() - 0.5); // 打乱牌序
    return deck;
}
exports.createDeck = createDeck;
// 打印一副扑克
function printDeck(deck) {
    let result = "底牌:";
    const desk = deck.splice(0, 3); // 底牌
    desk.forEach((poker, i) => {
        result += poker.color ? ` ${poker.color}${poker.number}` : ` ${poker.number}`;
    });
    result += '\n用户1:';
    deck.forEach((poker, i) => {
        if (i % 17 === 0 && i !== 0)
            result += `\n用户${i / 17 + 1}:`;
        result += poker.color ? ` ${poker.color}${poker.number}` : ` ${poker.number}`;
    });
    console.log(result);
}
exports.printDeck = printDeck;

编译结果中仅对 import { Colors, Nums } from "./enums";进行了编译;忽视了对 import { Deck } from "./types"; 的编译。