Typescript配置与编译
上一篇 Typescript入门指南 我们全面学习了ts的基础使用技巧,而掌握这些对于实战还是不够的,因为Typescript并不能直接被运行,往往要通过编译成目标js文件才可以被使用。ts的编译需要通过配置文件来控制,通过文件配置可以更好细粒度的定制编译结果。除此之外还要了解ts的命令行可执行文件 tsc
执行编译的操作
配置文件是全面掌握ts很关键的一环,读者一定要不断练习
小贴士
文章中涉及到的代码示例你都可以从 这里查看 ,若对你有用还望点赞支持;你可以在 TS Playground 尝试配置TS的编译结果
tsc
tsc 是 TypeScript 编译器的命令行工具,用于将 TypeScript 代码编译成 JavaScript 代码;也可以生成对应的 .d.ts
文件,用于描述 TypeScript 模块的类型信息等等
安装
tsc也是一个普通的npm包,你可以选择安装到全局或者某个项目中
npm install -g typescript # 全局
npm install typescript # 局部
2
安装好后查看版本:
➜ npx tsc --version
Version 4.9.5
2
运行
通过 tsc
可以将ts文件编译成js文件,运行语法如下:
tsc [...args] [目标文件]
# 如编译某个文件
tsc app.ts
2
3
4
当运行 tsc
时会查找当前目录(process.cwd()
)的 tsconfig.json
文件,并根据配置进行编译。如果当前工作目录没有找到对应的配置文件,则按目录向父级层层向上查找直到根目录,这一特点和nodejs解析一样的,如果最终还没有找到就会使用默认的配置文件值
用户也可以在执行命令行时传递一些参数,这些参数优先级高于配置文件
编译选项
以下是一些常用的 tsc 编译选项:
选项 | 描述 |
---|---|
--target | 指定编译目标(如 es5 、es6 ) |
--module | 指定模块化方案(如 commonjs 、es6 ) |
--outDir | 指定输出目录 |
--strict | 启用所有严格类型检查选项 |
--noImplicitAny | 禁止隐式的 any 类型 |
--declaration | 生成 .d.ts 声明文件 |
--sourceMap | 生成 .map 源映射文件,用于调试 |
--watch | 启用监视模式,自动重新编译 |
--removeComments | 移除注释 |
--jsx | 指定 JSX 的处理方式(如 preserve 、react ) |
这些选项不需要死记硬背,你可以随时查看官方命令行参数文档。通常情况都会使用配置文件来定义编译环境,这种方式一切都是可读的很好维护,而使用命令行的形式就会少很多,一般用作脚本执行、或者进行ts的相关测试使用命令行方式会便捷很多
比如使用 --watch
参数用于实时编译ts文件,这对于测试ts相关功能时非常有用
命令行参数适合简单的场景,而项目级别的配置还需要配置文件来管理
tsconfig.json
tsconfig.json
是 TypeScript 项目的核心配置文件,用于指定编译器选项和项目的结构。它定义了编译器(tsc)的行为,并为项目开发提供了灵活性和一致性。它是一个 JSON
文件,用来告诉 TypeScript 编译器如何处理 .ts
或 .tsx
文件
当项目中存在tsconfig.json
时只需运行 tsc 命令即可根据配置进行编译,而无需指定额外的命令行参数
如果你对js项目的工程化非常熟悉的话,应该也知道JS工程项目中也有 jsconfig.json 配置文件,可以来定制一些开发环境,而tsconfig也类似这个道理
初始化🚀
可以通过tsc --init
在对应的目录下生成默认的 配置文件,生成的文件内容大概如下:
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted
/* Modules */
"module": "commonjs", /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
// 省略...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
执行命令后会生成对应的配置文件,配置中列出了部分选项,并且都会有对应的注释,可以很方便的直到当前属性的含义。配置文件中同类功能属性会放在一起
整体结构
一个典型的 tsconfig.json
文件大致可以分为以下几部分:
compilerOptions
:编译器选项,配置 TypeScript 编译行为files
:明确指定要包含的文件列表include
:指定需要包含的文件或目录的模式exclude
:指定需要排除的文件或目录的模式references
:定义项目间的依赖关系,用于多项目构建
其中compilerOptions
是编译的关键核心部分,关于配置文件大部分也是讲它的
compilerOptions
compilerOptions 是 tsconfig.json 文件中最重要的部分,用于配置 TypeScript 编译器的行为。它包含了许多选项,可以控制如何将 TypeScript 代码编译为 JavaScript 代码,以及如何进行类型检查等
源码编译
源码编译主要是告诉ts如何将源码降级成js,得告诉编译器源码使用了什么语法、要编译成什么语法,针对于最终运行时目标环境适配
比如最常见的就是
target: "esnext" // 输出目标语法环境 es2015/es5/es3...
ts支持将源码编译成esnext
、commonjs
、amd
等等语法,来适应不同的运行时环境,这里就涉及到了模块引用,下文会讲解
你也可以使用jsx
,然后配置对应的jsx环境:
"jsx": "preserve",
"jsxFactory": "",
"jsxFragmentFactory": "",
"jsxImportSource": "",
2
3
4
当你使用了装饰器
语法时,需要添加对应的配置告诉编译器:
"experimentalDecorators": true, // 启用实验性的装饰器语法
"emitDecoratorMetadata": true, // 输出对应的元数据,通常结合reflect-meta使用
2
ts还支持定制化内置库支持,比如你的程序只使用了Array
,用不到DOM
,那么就可以通过lib
来告诉编译器我的代码只使用了这个内置库,其他的不需要;精确的告诉编译器使用到的库,加快编译速度
lib: ["ESNext.Array"]
如果没有指定任何东西,默认会根据编译目标自动补充上下文环境
构建输出
编译后就要输出对应的目标文件了,ts也提供了很多配置选项来精细控制输出内容。比如输出到哪里、目标文件、sourcemap以及对于编译输出时类型错误等对输出的影响控制等等
设置目标编译文件输出目录及文件
"outFile": "./bundle.js", // 将输出内容全部编译到文件中
"outDir": "./", // 目标文件路径
2
注意:只有module
为amd
时才支持输出到某个文件
类型声明的编译输出,前面我们知道js不能直接运行ts代码,编译后的代码将会去掉所有的类型编程js代码,但编译器也支持输出代码类型声明文件.d.ts文件
,通常在引用没有源代码或者非友好类型提示的库文件时有很友好的代码提示作用
"declaration": true, // 是否开启类型声明
"declarationMap": true, // 声明文件soucemap
"emitDeclarationOnly": true, // 仅仅生成声明文件而不生成目标js文件
"declarationDir": "./", // 声明文件输出目录,默认值为outDir的值
2
3
4
对输出内容格式调整
"removeComments": true, // 移除注释
"newLine": "crlf", // 换行符格式
2
对sourcemap控制
"sourceMap": true, // 开启sourcemap
"sourceRoot": "", // debugger时sourcemap位置
"mapRoot": "", // debugger时sourcemap位置
"inlineSourceMap": true, // 使用行内sourcemap
"inlineSources": true, // 采用base64将源码编译到目标js文件中
2
3
4
5
除此之外还有一些其他的输出控制
"noEmit": true, // 开启后不输出任何内容
"stripInternal": true, // 当使用jsdoc时不生成对应的类型声明
"preserveConstEnums": true, // 当使用const enum时不会输出对应的enum信息,而直接使用值代替,反之亦然
"noEmitHelpers": true, // 不生成工具函数如__extends等等
"noEmitOnError": true, // 当编译类型出错时不生成任何内容
2
3
4
5
模块
现在很多大小项目或多或少都会有很多业务,项目会根据业务进行拆分功能,而且也会使用第三方软件包,那么就出现了模块引用处理以及模块类型检查提示等问题
根据目标运行时环境可以设置生成对应的模块语法
"module": "ESNext", // esnext/AMD/CommonJS/UMD/NodeNext 等等
比如ESNext
就会使用对应的import/export
语法,而CommonJS
则会使用require
语法。我们来对比下几种模块语法生成的代码:
// ESNext
import { run } from "./run";
run();
// CommonJS
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const run_1 = require("./run");
(0, run_1.run)();
// AMD
define(["require", "exports", "./run"], function (require, exports, run_1) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
(0, run_1.run)();
});
// UMD
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "./run"], factory);
}
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const run_1 = require("./run");
(0, run_1.run)();
});
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
根目录与相对路径解析
"rootDir": "./", // 根路径
"baseUrl": "./", // 告诉编译器从哪个路径进行解析,进一步收缩编译范围
"rootDirs": [], // 多个根路径
2
3
设置根目录可以告诉编译器源码目录在哪里包含哪些东西,有时候引入模块不是绝对路径,可以通过设置baseUrl
告诉编译器直接从这里解析,比如:
import A from "../../../utils/a.js
不是绝对路径引用,使用起来难免会出现错误,那么就可以这样配置
"baseUrl": "./src"
这样就可以直接使用绝对路径应用import A from utils/a.js
路径映射
"paths": {
"@/*": ["src/*"],
"~/*": ["./*"]
},
2
3
4
使用路径映射后可以大大简化模块的引用,比如:import A from "../../utils/a.js"
=> import A from "@/utils/a.js"
小贴士
如果项目使用了打包工具如webpack、vite等则也需要配置对应的路径映射告诉打包工具如何解析模块,ts配置通常只是告诉它如何解析对应的类型声明
模块类型声明解析
"typeRoots": [], // 第三方模块从哪里开始查找对应的类型目录,默认从 node_modules/@types 下进行查找
"types": [], // 指明项目中用到的模块类型声明,如果写了对应值,ts中只会包含指定的类型,其他都不再有类型提示
2
通常不用指定types
或typeRoots
,如果对项目中使用到的模块有明确认识,填写对应的类型可以加快解析速度
模块查找方式
"moduleResolution": "node", // bundler/classic/node 等
用于告诉编译器如何查找第三方模块,默认值为node
模块查找方式,这个在node文章中讲过,可前往查看;classic
为早期的查找方式基本不怎么使用,而bundler
是为现代化打包工具准备的,比如对于文件结尾、第三方模块查找等等
其他模块解析配置
"allowUmdGlobalAccess": true, // 允许访问umd全局的变量
"moduleSuffixes": [], // 模块解析的文件后缀,填写后可以省略
"resolveJsonModule": true, // 允许解析json文件
"noResolve": true, // 不解析任何
2
3
4
类型检查
ts配置中类型检查类似于eslint对代码的编码规范进行约束,如果不符合规定就会提示报错,往往在统一团队协助开发有很大的帮助,而ts也有类似的配置来约束开发者的编码
使用类型检查需要将总开关开启:
"strict": true, // 启用严格的类型检查
如果不开启这个选项其他更细节的类型选项将不起任何作用。开启后可以进一步细分检查选项
"noImplicitAny": true, // 禁止隐式声明 any 类型
"strictNullChecks": true, // 启用严格的 null/undefined 检查
"strictFunctionTypes": true, // 严格检查函数类型
"strictBindCallApply": true, // 严格检查 bind/call/apply 的调用
"strictPropertyInitialization": true, // 严格检查类属性的初始化
"noImplicitThis": true, // 禁止隐式推导的 this 类型
"useUnknownInCatchVariables": true, // 将 catch 子句的变量类型设为 unknown
"alwaysStrict": true, // 始终以严格模式解析代码
"noUnusedLocals": true, // 禁止未使用的局部变量
"noUnusedParameters": true, // 禁止未使用的函数参数
"exactOptionalPropertyTypes": true, // 严格检查可选属性的类型
"noImplicitReturns": true, // 禁止函数隐式返回 undefined
"noFallthroughCasesInSwitch": true, // 禁止 switch 语句的 case 贯穿(fallthrough)
"noUncheckedIndexedAccess": true, // 严格检查索引访问的安全性
"noImplicitOverride": true, // 禁止隐式覆盖父类方法(需显式使用 override 关键字)
"noPropertyAccessFromIndexSignature": true, // 禁止通过点语法访问索引签名属性
"allowUnusedLabels": true, // 允许未使用的代码标签(如循环标签)
"allowUnreachableCode": true, // 允许不可达代码(永远不会执行的代码)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这里不一一介绍了,可以根据选项注释结合项目使用进行调整
其他
还有一些配置
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"preserveSymlinks": true,
"forceConsistentCasingInFileNames": true,
"skipDefaultLibCheck": true,
"skipLibCheck": true
2
3
4
5
6
7
isolatedModules:启用隔离模块,确保每个模块独立运行,不引入其他模块的依赖或冲突。适用于非 TypeScript 编译工具,如 Babel。这有助于提高安全性,强制每个文件必须有明确的导入/导出语句(不能是全局脚本)。在高度依赖型或大型项目中,以避免潜在的安全风险和冲突
allowSyntheticDefaultImports:允许从没有默认导出的模块中默认导入(如 CommonJS 模块)。解决某些 CommonJS 模块(如 React)无法直接通过
import React from 'react'
导入的问题,需配合esModuleInterop: true
使用esModuleInterop:启用内部分析,改进对 CommonJS/AMD/UMD 模块的互操作性,提高开发速度和代码质量,但可能导致性能下降。禁用后可能提升安全性。启用以加速开发并利用TypeScript的最佳实践;禁用以提高构建速度或在需要更安全的环境时
preserveSymlinks:保持符号链接,确保项目结构和生产环境的一致性。这对于部署和结构完整性很重要。在需要保留项目结构不变的情况下,如在生产环境中使用
forceConsistentCasingInFileNames:统一文件名的大小写,提高团队协作的效率,避免大小写混乱。适用于开发团队要求文件命名一致的情况,以促进协作和维护
skipDefaultLibCheck:跳过对默认库(如 lib.d.ts)的类型检查
skipLibCheck:跳过对所有声明文件(.d.ts)的类型检查,大幅提升编译速度(尤其当项目依赖大量第三方类型声明时),第三方类型声明存在冲突或错误时的临时规避
references
在ts的配置中,references
是用于实现 项目引用(Project References) 的功能。它的核心目的是将大型 ts 项目拆分为多个相互依赖的子项目(子模块),从而优化构建性能、代码组织和类型管理
当项目庞大时,每次全量编译非常耗时。通过拆分项目,可以只重新编译修改过的子模块,俗称增量编译
,和目前turbo、nx等类似
增量编译在大型项目尤其是Monorepo之类的仓库中发挥着更大的作用,那么使用ts如何进行增量编译呢
假如我们有一个my-project
的Monorepo项目,它的目录结构如下:
my-project
├─ app
│ ├─ src
│ │ └─ main.ts
│ └─ tsconfig.json
├─ utils
│ ├─ src
│ │ └─ app.ts
│ └─ tsconfig.json
└─ tsconfig.base.json # 共享基础配置
2
3
4
5
6
7
8
9
10
如果我们要编译时需要切换到不同的目录对每个目录逐个进行编译,操作步骤会变得非常繁琐。而且当我们的仓库模块越来越多时每次模块的修改都需要对整体的仓库进行编译,如果知道哪个修改了编译哪个会有记忆负担;仓库代码体量变大时明显会降低编译速度
那么这时候就可以使用ts的references
进行增量编译,它的本质就是将某模块作为一个模块的子模块,只有子模块的内容有调整时,才会重新编译子模块,否则就会跳过,这种方式会提高编译速度
来调整下代码结构:
上面utils
模块将作为app
的子模块使用,如果某个模块要作为子模块,则必须设置composite:true
,以及"declaration": true
,那么utils/tsconfig.json
的配置如下:
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"rootDir": "./src",
"outDir": "./dist",
"declaration": true,
"declarationMap": true,
},
"include": ["./**/*"],
"exclude": ["dist"]
}
2
3
4
5
6
7
8
9
10
11
12
在app
项目中引用utils,那么app/tsconfig.json
的配置如下:
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
},
"references": [
{ "path": "../utils/tsconfig.json" }
],
"include": ["./**/*"],
"exclude": ["dist"]
}
2
3
4
5
6
7
8
9
10
11
12
13
接下来就来构建项目,ts提供了tsc -b 目录/指定的config配置
来构建指定项目,我们先来尝试在根目录下构建:
tsc -b app --verbose
执行命令后ts会根据references的引用顺序进行构建,可以看出先构建了utils,再构建app
构建完毕后会生成对应的产物,其中在每个项目中会生成一个.tsbuildinfo
文件,此文件记录着当前项目的文件信息,类似于文件指纹信息,再下次构建时进行对比,避免重复构建
现在我们对源码不做任何改动,再次执行命令,会发现立马执行结束,也就是说直接跳过,不用构建
读者感兴趣可以尝试修改代码后,再次构建发生的情况
tsbuildinfo
文件会在每个项目下都有生成,感觉会源码视觉效果有干扰,我们可以通过配置将其放在统一位置。在每个配置文件中加入以下配置:
{
"compilerOptions": {
"tsBuildInfoFile": "../.cache/tsconfig.app.tsbuildinfo"
},
}
2
3
4
5
这里将tsbuildinfo
文件都放到根路径下的.cache/
目录里,注意稍微修改下tsbuildinfo
文件名,避免多个项目文件冲突
以上改动基本完成了增量编译调整,但是每次执行命令时都需要指定编译哪个项目或者进入到某个目录再进行编译,如果有多个主模块将会变得麻烦‼️
想想是不是可以在根目录下创建一个tsconfig.json
文件,将然后将所有的子项目放入references
中,让ts主动找到每个项目去编译是不是更好呢❓我们来稍微优化下:
my-project
├─ app
│ ├─ src
│ │ └─ main.ts
│ └─ tsconfig.app.json
├─ utils
│ ├─ src
│ │ └─ app.ts
│ └─ tsconfig.utils.json
├─ tsconfig.json # 项目主入口配置
└─ tsconfig.base.json # 共享基础配置
2
3
4
5
6
7
8
9
10
11
我们在根目录下新增了tsconfig.json
配置:
{
"files": [],
"references": [
{"path": "./app/tsconfig.app.json"},
{"path": "./utils/tsconfig.utils.json"},
]
}
2
3
4
5
6
7
注意files:[]
(或者include:[]
)是必须的‼️ 目的是告诉ts当前目录不编译任何东西,编译器只需要根据引用去编译即可
接下来只需要在终端根目录下执行:
tsc --build
如果没错的话会生成一样的内容
到此为止关于ts的references增量编译就差不多了
其他配置
除此之外可能还有一些配置上文没有涉及到,不过都非常简单,相信读者基本都懂
files: [],
include: [],
exclude: [],
extends: ""
2
3
4
总结
本文从ts的配置文件的创建循序渐进,主要通过几个方面对compilerOptions
选项进行了全面认识,并结合小案例说明了references
增量编译的重要性,希望读者能多加练习;接下来我们来继续学习ts的类型声明以及在第三方包中的常见技巧