會寫 TypeScript 但你真的會 TS 編譯配置嗎?
最近遇到了挺多涉及到前端“編譯”方面的工作,其中關(guān)于 TypeScript 的編譯會涉及到關(guān)于 tsconfig.json 文件的配置,由于配置項繁雜,遂逐一解析并驗證,減少大家的一些疑惑,并提升工作效率!
隨著 TypeScript 的流行,越來越多的項目通過使用 TypeScript 來實現(xiàn)編寫代碼時候的類型提示和約束,從開發(fā)過程中減少 BUG 出現(xiàn)的概率,以此提升程序的健壯性和團(tuán)隊的研發(fā)效率。
為什么會單獨寫一篇文章來講述 tsconfig.json 文件的配置吶?原因是筆者在做 TS 項目的時候,由于對其中的配置項不熟悉,搞來搞去,搞好久,煩死了!所以決定好好梳理下。
越來越多的項目用上了 TypeScript,因此如何按需配置 tsconfig 也應(yīng)該是前端工程師需要掌握的技能之一。
本文內(nèi)容結(jié)構(gòu)如下,朋友們按需食用:
一、前置知識
在熟悉掌握 tsconfig.json 文件配置前,先給首次接觸 TS 的同學(xué)預(yù)備一下“前置知識”。
1.1 TypeScript 是什么?
TypeScript 官網(wǎng):https://www.typescriptlang.org
TypeScript 是一種基于 JavaScript 的強(qiáng)類型編程語言,它使得在前端項目開發(fā)過程中更加嚴(yán)謹(jǐn)且流暢,一定程度上保證了大型前端項目程序的健壯性。
- TypeScript 是由微軟開發(fā)的一款開源的編程語言;
- TypeScript 是 JavaScript 的超集,遵循最新的 ESM 規(guī)范,TypeScript 擴(kuò)展了 JavaScript 的語法;
- TypeScript 更像后端 JAVA、C# 這樣的面向?qū)ο笳Z言,可以讓 JS 開發(fā)大型企業(yè)級項目。
但是 TypeScript 并不可以直接運行,而是需要轉(zhuǎn)換成 JavaScript 代碼才可以在 Node.js 或瀏覽器環(huán)境下執(zhí)行,因此我們需要通過“編譯器”將 TS 代碼轉(zhuǎn)換為 JS 代碼。
1.2 什么是 tsc ?
tsc 的全稱是 TypeScript Compiler,也就是將 TypeScript 轉(zhuǎn)碼為 JavaScript 代碼的編譯器。
tsc 的全局安裝方式:
- npm install typescript -g
當(dāng)我們編譯一份 index.ts 文件時,會使用下面的命令:
- tsc ./index.ts
這樣就可以得到一份編譯成為 JavaScript 代碼的 ./index.js 文件。
tsc 實際就是將 TS 轉(zhuǎn)為 JS 的編譯(器)腳手架工具,如果是一個 TS 的前端工程項目,那么就可以通過項目中的 tsconfig.json 文件來自定義配置 TS 編譯相關(guān)規(guī)則。
項目中的 tsconfig.json 文件,我們一般會通過如下快捷命令生成:
- tsc --init
執(zhí)行完后,會在項目根目錄生成一個簡單的初始化 tsconfig.json 配置描述文件,如果沒有特別的要求,該初始化配置就足以支持你愉快地使用 TS 開發(fā)啦!
更多相關(guān) TS 編譯配置和使用說明可以通過 tsc -h 查看。
1.3 tsconfig.json 文件
tsconfig.json[1] 文件是用于描述將 TypeScript 轉(zhuǎn)為 JavaScript 代碼的配置文件。
IDE(代碼編輯器)將會根據(jù) tsconfig.json 文件來對當(dāng)前項目中支持不同程度的類型約束,同時也是對 TSC 編譯 TypeScript 代碼過程做一些預(yù)定義、約束入口和編譯輸出目錄等配置。
因此對于一個支持 TypeScript 編程語言的工程來說,tsconfig.json 文件就是編碼的基礎(chǔ)。
二、tsconfig.json 配置詳解
有了上面的前置知識作為基石,相信大家會對 tsconfig.json 文件的配置項也會更加容易理解。
- tsconfig 的詳細(xì)配置:https://www.typescriptlang.org/tsconfig
- tsconfig 的協(xié)議描述網(wǎng)址:http://json.schemastore.org/tsconfig
tsconfig 協(xié)議
筆者將從常見的配置項單獨解釋,然后在最后會將一些不常用的配置統(tǒng)一解釋,朋友們可以將這篇文章收藏一下,可當(dāng)作一份 tsconfig 配置的中文查詢對照表 👀。
2.1 files
files 字段用于指明需要 tsc 編譯的一個或多個 ts 文件,例如:
- {
- "files": ["index.ts", "global.d.ts"],
- }
當(dāng)指定的文件或文件夾不存在時,會提示 ❌ 錯誤!
2.2 include
include 字段用于指明需要被 tsc 編譯的文件或文件夾列表,例如:
- {
- "include": [
- "src",
- "global.d.ts"
- ],
- }
2.3 exclude
exclude 字段用于排除不需要 tsc 編譯的文件或文件夾列表,例如:
- {
- "exclude": ["test.ts", "src/test.ts"],
- }
注意: exclude 字段中的聲明只對 include 字段有排除效果,對 files 字段無影響,即與 include 字段中的值互斥。
如果 tsconfig.json 文件中 files 和 include 字段都不存在,則默認(rèn)包含 tsconfig.json 文件所在目錄及子目錄的所有文件,且排除在 exclude 字段中聲明的文件或文件夾。
2.4 compileOnSave
compileOnSave 是聲明是否需要在保存時候自動觸發(fā) tsc 編譯的字段,一般來說,我們的代碼編譯過程會通過 Rollup、Webpack 等打包構(gòu)建工具,并且使用熱更新,因此無需配置該項,保持缺省即可。
- {
- "compileOnSave": false,
- }
2.5 extendsextends
字段用于指明繼承已有的 tsconfig 配置規(guī)則文件。
該字段可以說是非常有用了,因為我們的 tsconfig 配置其實各個項目之間大同小異,因此完全可以結(jié)合自己團(tuán)隊的情況,抽離一個基礎(chǔ)且公共的 tsconfig 配置,并將其發(fā)包,然后作為 extends 字段的值來繼承配置。
tsconfig 推薦默認(rèn)配置可以參考官方的包:@tsconfig/recommended[2]
@tsconfig/recommended 的配置如下:
- {
- "compilerOptions": {
- "target": "ES2015",
- "module": "commonjs",
- "strict": true,
- "esModuleInterop": true,
- "skipLibCheck": true,
- "forceConsistentCasingInFileNames": true
- },
- "$schema": "https://json.schemastore.org/tsconfig",
- "display": "Recommended"
- }
例如繼承一個發(fā)包后的 tsconfig 基礎(chǔ)配置,并通過顯示聲明編譯的目標(biāo)代碼版本為 ES2016 來覆蓋覆蓋 @tsconfig/recommended 中對應(yīng)配置項。
- {
- "extends": "@tsconfig/recommended/tsconfig.json",
- "compilerOptions": {
- "target": "ES2016"
- }
- }
作為一些實踐經(jīng)驗,社區(qū)也提供了一些常見環(huán)境(例如:Nuxt、Vite、Node 等)最佳實踐后的基礎(chǔ)配置,推薦參閱:https://github.com/tsconfig/bases/[3]
2.6 compilerOptions
compilerOptions 是一個描述 TypeScript 編譯器功能的“大”字段,其值類型是“對象”,因此包含了很多用于描述編譯器功能的子字段,其子字段的功能如下:
(1). target
target 字段指明經(jīng)過 TSC 編譯后的 ECMAScript 代碼語法版本,根據(jù) ECMAScript 語法標(biāo)準(zhǔn),默認(rèn)值為 ES3。
TypeScript 是 JavaScript 的超集,是對 JavaScript 語法和類型上的擴(kuò)展,因此我們可以使用 ES5、ES6,甚至是最新的 ESNext[4] 語法來編寫 TS。例如當(dāng)我們使用 ES2021 語法來編碼 TS 文件,同時配置如下:
- {
- "compilerOptions": {
- "target": "ES5",
- }
- }
則會將對應(yīng)使用了最新 ECMAScript 語法的 TS 文件編譯為符合 ES5 語法規(guī)范的 *.js 文件。
延伸一下知識點,思考一下 tsc 是如何將高版本(ECMAScript 規(guī)范)代碼向低版本代碼轉(zhuǎn)換的?這個轉(zhuǎn)換的結(jié)果靠譜嗎?與 Babel 有何差異?
一圖看 ECMAScript 各版本功能差異
另外對于個版本差異有想簡單了解的👬,可以閱讀《1.5萬字概括ES6全部特性[5]》
通過一個實驗,在 src/index.ts 文件中使用了 Map、Async/Await、Promise、擴(kuò)展運算符,并在 tsconfig.jon -> target 設(shè)置為 ES5:
驗證 target 降級處理
然后發(fā)現(xiàn)在右側(cè)的 dist/index.js 文件中,依然存在 new Map() 、Promise 語法,因此可以得出結(jié)論:tsc 的代碼降級編譯并不能完全處理兼容性。
通過官方文檔了解到:
這里提到了 lib 字段,意思是 target 不同的值會有對應(yīng)默認(rèn)的 lib 字段值,當(dāng)然也支持開發(fā)者顯示指明 lib 字段的值,那么接下來看看 lib 是干嘛的吧!
(2). lib
lib 字段是用于為了在我們的代碼中顯示的指明需要支持的 ECMAScript 語法或環(huán)境對應(yīng)的類型聲明文件。
例如我們的代碼會使用到瀏覽器中的一些對象 window、document,這些全局對象 API 對于 TypeScript Complier 來說是不能識別的:
lib 未顯示引入 DOM 會提示類型錯誤
因而需要在 lib 字段中如下配置:
- {
- "compilerOptions": {
- "target": "ES5",
- "lib": ["ES5", "ES6", "DOM"],
- }
- }
來顯式引入在 DOM 即瀏覽器環(huán)境下的一些默認(rèn)類型定義,即可在代碼中使用,window、document 等瀏覽器環(huán)境中的對象,TS 在運行時以及編譯時就不會報類型錯誤。
引入類型定義后無錯誤提示
綜合 target 和 lib 字段的實際功能表現(xiàn),我們可以得出結(jié)論:
TSC 的編譯結(jié)果只有部分特性做了 pollyfill 處理,ES6[6] 的一些特性仍然被保留,想要支持完全的降級到 ES5 還是需要額外引入 pollyfill(也就是我們在項目的入口文件處 import 'core-js'),但建議是將 target 字段值設(shè)置為 ES6,提升 TSC 的速度。
因此,筆者對于使用 TSC 編譯的觀點是:
不應(yīng)該將 TSC 作為編譯項目的工具,應(yīng)該將 TSC 作為類型檢查工具,代碼編譯的工作盡量交給 Rollup、Webpack 或 Babel 等打包工具!
另外推薦閱讀《為什么說用 babel 編譯 typescript 是更好的選擇》
(3). module
module 字段指明 tsc 編譯后的代碼應(yīng)該符合何種“模塊化方案”,可以指定的枚舉值有:none, commonjs, amd, system, umd, es2015, es2020, 或 ESNext,默認(rèn)值為 none。
在如今的前端開發(fā)趨勢來講,主要是使用 ESM、CommonJS、UMD、IIFE 四種模塊化方案,未來會趨向于 ESM,當(dāng)然我們會根據(jù)項目的應(yīng)用場景來決定使用何種模塊化方案,例如:NodeJS 使用 CommonJS,瀏覽器里可以使用 ESM,不過現(xiàn)在的打包工具,會自動處理 CommonJS 和 ESM 的差異,并包裝成符合指定模塊化規(guī)范的代碼,
在 tsconfig.json 可以設(shè)置 allowSyntheticDefaultImports 字段為 true,來允許合成默認(rèn)導(dǎo)入。
(4). esModuleInterop
簡單來說,就是支持合成默認(rèn)導(dǎo)入。
在前端項目開發(fā)時,使用 ESM 編寫代碼引入了 CJS 的模塊,由于 CJS 模塊沒有默認(rèn)導(dǎo)出內(nèi)容,因此需要通過我們的工具去自動化合成 CJS 的默認(rèn)導(dǎo)出,以支持在 ESM 下流暢開發(fā)。
參閱文章《esModuleInterop 到底做了什么?[7]》,講得非常詳細(xì)也非常好。
當(dāng) esModuleInterop 字段設(shè)置為 true 時候,上述提到的 allowSyntheticDefaultImports 字段也會自動設(shè)置為 true。
(5). moduleResolution
moduleResolution 聲明如何處理模塊,枚舉值:classic、node,會根據(jù) module 字段決定默認(rèn)值。
推薦手動設(shè)置為 node,更符合現(xiàn)在大家的編碼認(rèn)識一些,而且大部分的構(gòu)建打包工具都是基于 Node。
舉個,遇到 import {a} from 'a-lib'; 這樣的模塊引入代碼應(yīng)該如何去(解析)查找到對應(yīng)的模塊文件。
(6). baseUrl & paths
baseUrl:設(shè)置基本目錄以解析非絕對模塊名稱(定義一個根目錄,以此進(jìn)行絕對文件路徑解析)
paths:用于設(shè)置模塊名或路徑映射列表,這樣就可以簡寫項目中自定義模塊的文件路徑。
舉一個 :
- {
- "compilerOptions": {
- // 注意:baseUrl 必選,與 paths 成對出現(xiàn),以 tsconfig.json 文件所在目錄開始
- "baseUrl": ".",
- "paths": {
- // 映射列表
- "@/*": [
- "src/*"
- ],
- "moduleA": [
- "src/libs/moduleA"
- ]
- }
- }
- }
- // 代碼里這么寫
- import Toast from '@/components/Toast.ts' // 模塊實際位置: src/components/Toast.ts
- import TestModule from 'moduleA/index.js' // 模塊實際位置: src/libs/moduleA/index.js
⚠️ 注意: 如果需要自動生成(導(dǎo)出)類型定義文件,TSC 不會處理路徑別名,需要引入 typescript-transform-paths[8] 插件,以及 TTypescript[9] 來轉(zhuǎn)換路徑別名為相對路徑。
由于當(dāng)前的 TypeScript 不支持 tsconfig.json 中的自定義轉(zhuǎn)換器,且無法使用 tsc 命令使用自定義轉(zhuǎn)換器編譯文件,所以引入了 TTypescript 作為包裝器
- // tsconfig.json
- {
- "compilerOptions": {
- "baseUrl": "./",
- // 配置路徑別名映射
- "paths": {
- "@/*": ["src/*"]
- },
- "plugins": [
- // 轉(zhuǎn)換輸出 js 文件中的路徑
- { "transform": "typescript-transform-paths" },
- // 轉(zhuǎn)換輸出 .d.ts 文件中的路徑
- { "transform": "typescript-transform-paths", "afterDeclarations": true }
- ]
- }
- }
plugins[10] 是用于擴(kuò)展 TSC 編譯器功能的字段。
例如在 Rollup 打包環(huán)境下,可以如下配置:
- import typescript from '@rollup/plugin-typescript';
- import ttypescript from 'ttypescript';
- export default [
- {
- input: './src/index.ts',
- output: {
- dir: 'dist',
- format: 'cjs',
- entryFileNames: 'index.js',
- },
- plugins: [
- typescript({
- typescript: ttypescript,
- }),
- ],
- },
- ];
如果是有自動導(dǎo)出類型定義文件的需求,才需要搞這一套插件~
(7). rootDir & outDir
rootDir:指定 TypeScript 識別讀取的根目錄,用于所有非聲明輸入文件的最長公共路徑
例如:'"rootDir": "./src",則 src 目錄下的 TS 文件不能引用 src 目錄以外的 ts 文件,一般我們會設(shè)置為 ./src 或 ./(即 tsconfig.json 所在目錄)
outDir:輸出目錄,即 tsc 編譯后的文件輸出的文件夾路徑(基于 tsconfig.json 文件的相對路徑)
例如:"outDir": "./dist",及將 TSC 編譯輸出的 JS 文件,統(tǒng)一輸出的 ./dist 目錄下。
(8). jsx
如果是有 jsx 語法需要支持的項目,可以設(shè)置值 preserve、react 等
- {
- "compilerOptions": {
- "jsx": "preserve", // 一般 preserve 即可
- },
- }
(9). importHelpers
importHelpers 決定是否啟用從 tslib 庫引入語法降級輔助函數(shù),以避免重復(fù)冗余的輔助函數(shù)聲明。
個人建議是設(shè)置為 true 來啟用。
(10).experimentalDecorators
experimentalDecorators 用于聲明是否啟實驗性用裝飾器模式。
TypeScript 和 ES6 中引入了 Class 的概念,同時在 Decorators[11] 提出了裝飾器模式,通過引入裝飾器模式,能極大簡化書寫代碼。
當(dāng)前對于 Decorator 的支持性不太好,如果是一些涉及到使用了裝飾器的需要,就需要開啟這個屬性。
(11). noEmit
noEmit 設(shè)置是否輸出 js 文件,一般是設(shè)置為 false,將打包等工作交給 Webpack 等工具。
三、tsconfig.json 全解析
上面針對 tsconfig.json 中一些常見配置做了詳細(xì)解釋,將一些不常用的配置字段組合在一起,做一個 Checklist 如下:
- {
- "compilerOptions": {
- /* 基本選項 */
- "target": "es6", // 指定 ECMAScript 目標(biāo)版本: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
- "module": "commonjs", // 指定使用模塊: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
- "lib": [], // 指定要包含在編譯中的庫文件
- "allowJs": true, // 允許編譯 javascript 文件
- "checkJs": true, // 報告 javascript 文件中的錯誤
- "jsx": "preserve", // 指定 jsx 代碼的生成: 'preserve', 'react-native', or 'react'
- "declaration": true, // 生成相應(yīng)的 '.d.ts' 文件
- "declarationDir": "./dist/types", // 生成的 '.d.ts' 文件保存文件夾
- "sourceMap": true, // 生成相應(yīng)的 '.map' 文件
- "outFile": "./", // 將輸出文件合并為一個文件
- "outDir": "./dist", // 指定輸出目錄
- "rootDir": "./", // 用來控制輸出目錄結(jié)構(gòu) --outDir.
- "removeComments": true, // 刪除編譯后的所有的注釋
- "noEmit": true, // 不生成輸出文件
- "importHelpers": true, // 從 tslib 導(dǎo)入輔助工具函數(shù)
- "isolatedModules": true, // 將每個文件做為單獨的模塊 (與 'ts.transpileModule' 類似).
- /* 嚴(yán)格的類型檢查選項 */
- "strict": true, // 啟用所有嚴(yán)格類型檢查選項
- "noImplicitAny": true, // 在表達(dá)式和聲明上有隱含的 any類型時報錯
- "strictNullChecks": true, // 啟用嚴(yán)格的 null 檢查
- "noImplicitThis": true, // 當(dāng) this 表達(dá)式值為 any 類型的時候,生成一個錯誤
- "alwaysStrict": true, // 以嚴(yán)格模式檢查每個模塊,并在每個文件里加入 'use strict'
- /* 額外的檢查 */
- "noUnusedLocals": true, // 有未使用的變量時,拋出錯誤
- "noUnusedParameters": true, // 有未使用的參數(shù)時,拋出錯誤
- "noImplicitReturns": true, // 并不是所有函數(shù)里的代碼都有返回值時,拋出錯誤
- "noFallthroughCasesInSwitch": true, // 報告switch語句的fallthrough錯誤。(即,不允許switch的case語句貫穿)
- /* 模塊解析選項 */
- "moduleResolution": "node", // 選擇模塊解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
- "baseUrl": "./", // 用于解析非相對模塊名稱的基礎(chǔ)目錄
- "paths": {}, // 模塊名到基于 baseUrl 的路徑映射的列表
- "rootDirs": [], // 根文件夾列表,其組合內(nèi)容表示項目運行時的結(jié)構(gòu)內(nèi)容
- "typeRoots": [], // 包含類型聲明的文件列表
- "types": [], // 需要包含的類型聲明文件名列表
- "allowSyntheticDefaultImports": true, // 允許從沒有設(shè)置默認(rèn)導(dǎo)出的模塊中默認(rèn)導(dǎo)入。
- "esModuleInterop": true, // 支持合成模塊的默認(rèn)導(dǎo)入
- /* Source Map Options */
- "sourceRoot": "./", // 指定調(diào)試器應(yīng)該找到 TypeScript 文件而不是源文件的位置
- "mapRoot": "./", // 指定調(diào)試器應(yīng)該找到映射文件而不是生成文件的位置
- "inlineSourceMap": true, // 生成單個 soucemaps 文件,而不是將 sourcemaps 生成不同的文件
- "inlineSources": true, // 將代碼與 sourcemaps 生成到一個文件中,要求同時設(shè)置了 --inlineSourceMap 或 --sourceMap 屬性
- /* 其他選項 */
- "experimentalDecorators": true, // 啟用裝飾器
- "emitDecoratorMetadata": true // 為裝飾器提供元數(shù)據(jù)的支持
- },
- /* 指定編譯文件或排除指定編譯文件 */
- "include": ["src/**/*"],
- "exclude": ["node_modules", "**/*.spec.ts"],
- "files": ["index.ts", "test.ts"],
- // 從另一個配置文件里繼承配置
- "extends": "@tsconfig/recommended",
- // 讓 IDE 在保存文件的時候根據(jù) tsconfig.json 重新生成文件
- "compileOnSave": true // 支持這個特性需要Visual Studio 2015, TypeScript 1.8.4 以上并且安裝 atom-typescript 插件
- }
四、打包工具中的 TypeScript
前文講到了為什么不推薦直接使用 TSC 作為項目的打包編譯工具,那么接下來就簡單看看在常見的幾款打包工具中針對 TypeScript 的編譯方案是如何設(shè)計的?
4.1 Rollup + TypeScript
在 Rollup 打包中,我們一般只需要添加 @rollup/plugin-typescript[12] 插件即可,該插件會默認(rèn)讀取項目根目錄下的 tsconfig.json 配置文件。
Rollup 的配置就像這樣:
- // file: rollup.config.js
- import typescript from '@rollup/plugin-typescript';
- export default {
- input: 'src/index.ts',
- output: {
- dir: 'output',
- format: 'cjs'
- },
- plugins: [typescript()]
- };
結(jié)合其源碼:
默認(rèn)使用 TSC 作為 TS 的編譯器
因為 typescript 聲明了是 peerDependencies,因此會采用項目中安裝的 typescript 版本,即是使用我們項目中的 TS 編譯器。
通過閱讀 @rollup/plugin-typescript 源碼,可以看到該插件會默認(rèn)使我們自己項目中的 tsconfig.json 文件作為 TSC 編譯的配置,但會做一些配置預(yù)設(shè)覆蓋:
會調(diào)用 ts.parseJsonConfigFileContent() 方法,將 FORCED_COMPILER_OPTIONS 值 merge 到用戶的自定義配置中。
FORCED_COMPILER_OPTIONS
通過英文解釋看到,因為需要 TSC 編譯獲得 JS 產(chǎn)物,所以會將 noEmit 設(shè)置為 false,也就是 TSC 編譯會輸出文件,但為什么我們在輸出目錄卻沒有看到對應(yīng)的 TSC 產(chǎn)物吶?
TSC 編譯結(jié)果存儲到內(nèi)存中
但是如果開啟了 declaration,則會將 TSC 解析得到的 *.d.ts 文件輸出到指定目錄。
4.2 Webpack + TypeScript
在 Webpack 中的 TypeScript[13] 官方文檔中,指明了需要安裝:typescript 和 ts-loader 兩個模塊。
配置 Webpack 并支持 TypeScript 的配置如下:
- // file: webpack.config.js
- const path = require('path');
- module.exports = {
- entry: './src/index.ts',
- module: {
- rules: [
- {
- test: /\.tsx?$/,
- use: 'ts-loader',
- exclude: /node_modules/,
- },
- ],
- },
- resolve: {
- extensions: ['.tsx', '.ts', '.js'],
- },
- output: {
- filename: 'bundle.js',
- path: path.resolve(__dirname, 'dist'),
- },
- };
可以看出 Webpack 主要是依賴 ts-loader 實現(xiàn)對 TypeScript 語法的編譯支持,再看看對 ts-loader 的介紹:
ts-loader
換句話說,ts-loader 實際調(diào)用了 TSC 來編譯 TS 文件,TSC 的配置依賴于你項目中的 tsconfig.json 文件。
如果使用了 Babel,則可以使用 @babel/preset-typescript[14] 來處理,但 Babel 不會做 TS 類型校驗,在打包工具 Rollup 和 Webpack 中都可以引入 Babel,那么接下來看看 Babel 是如何處理 TypeScript 的吧!
4.3 Babel + TypeScript
Babel 處理 TS 需要安裝 @babel/preset-typescript 模塊,然后在 babel 項目配置文件中聲明:
- // 配置說明:https://babeljs.io/docs/en/babel-preset-typescript
- {
- "presets": ["@babel/preset-typescript"]
- }
但 Babel 中只會對 TS 代碼轉(zhuǎn)為 JS 代碼(通過 parse TS 文件為 AST,并直接移除類型信息,然后打印目標(biāo)代碼),不會去做 TS 類型檢查,所以 Babel 編譯 TS 文件相較于 TSC 的速度更快!
同時,因為 Babel 會根據(jù)不同的兼容環(huán)境,按需引入 pollyfill,比 TSC 直接引入 core-js 更優(yōu)雅,因此使用了 Babel 打包的體積也會更小。
TS 類型檢查工作可以交給代碼編輯器承擔(dān),當(dāng)然同時可以新增 TS 檢查的命令:
- // package.json
- {
- "script": {
- "tsCheck": "tsc --noEmit",
- }
- }
可以把類型檢查放到特定的 npm scripts 生命周期之前,另外其實也可以將類型檢查放到 git commit 階段,用于做必要的 TS 類型檢查,保證項目的正確性。
4.4 ESbuild + TypeScript
通過 Vite 體會到了 ESbuild[15] 帶來的開發(fā)熱更新“極速”體驗,針對 TS 項目,ESbuild 和 Babel 是相同的編譯策略,即僅編譯,不校驗類型。
ESbuild 處理 TypeScript[16] 同樣可以帶來飛一般的感覺!
Vite 使用 esbuild 將 TypeScript 轉(zhuǎn)譯到 JavaScript,約是 tsc 速度的 20~30 倍,同時 HMR 更新反映到瀏覽器的時間小于 50ms。—— Vite Docs[17]
但在 ESbuild 中需要啟用 tsconfig 中的 isolatedModules 功能,然后在類型引入的時候需要替換,規(guī)則參考如下:
- // old
- import { UserType } from './types';
- // new
- import type { UserType } from './types';
因為 ESbuild 是單獨編譯每個文件,無法判斷引入的是 Type(類型) 還是 值,所以需要開發(fā)者顯示地聲明是“Type”。
同時還需要啟用 esModuleInterop 功能,用于支持 ESM 模塊合成默認(rèn)導(dǎo)入,以兼容 CJS 和 ESM 規(guī)范。
另外 ESbuild 不支持:emitDecoratorMetadat、const enum 類型和 *.d.ts 文件
此外,關(guān)注到兼容性處理這方面,Bable 和 ESbuild 是類似的,因此會存在兼容性問題:
兼容性
對于裝飾器處理不支持,因為 TS 是 JS 的超集,ESnext 的規(guī)范提案某些還不是穩(wěn)定的,因此如果有這方面訴求的項目,可以借助 TSC 做預(yù)編譯,例如使用 Rollup 的 typescript 插件 或 Webpack 的 ts-loader 方式。
五、總結(jié)
針對 TypeScript 項目的類型檢查和編譯流程算是完整過了一遍,相信已足以支撐大家在工作中自定義化配置 TS 前端項目!
另外,tsconfig.json 推薦配置策略如下:
- 借助 extends 字段,并結(jié)合項目應(yīng)用場景,繼承官方推薦配置
- 針對項目特點,按需修改對應(yīng)功能配置
- 建議啟用 importHelpers、esModuleInterop,取消 noEmit 輸出
- TS 項目的打包構(gòu)建,推薦使用 Webpack、Rollup、Bable 等專業(yè)工具來保證正確性和構(gòu)建優(yōu)化
參考資料
[1]TSconfig.json 手冊: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html
[2]@tsconfig/recommended: https://www.npmjs.com/package/@tsconfig/recommended
[3]TSconfig 最佳實踐配置: https://github.com/tsconfig/bases/
[4]ES.Next 語法提案: https://github.com/tc39/proposals
[5]1.5萬字概括ES6全部特性: https://watesegmentfault.com/a/1190000020678240
[6]ES6 語法新特性: http://es6-features.org/#Constants
[7]esModuleInterop 到底做了什么?: https://zhuanlan.zhihu.com/p/148081795
[8]typescript-transform-paths: https://www.npmjs.com/package/typescript-transform-paths
[9]TTypescript: https://www.npmjs.com/package/ttypescript
[10]compilerOptions.plugins: https://www.typescriptlang.org/tsconfig#plugins
[11]Decorators: https://github.com/tc39/proposal-decorators
[12]@rollup/plugin-typescript: https://github.com/rollup/plugins/tree/master/packages/typescript/#readme
[13]Webpack 中的 TypeScript: https://webpack.docschina.org/guides/typescript/
[14]@babel/preset-typescript: https://babeljs.io/docs/en/babel-preset-typescript
[15]ESbuild: https://esbuild.github.io/
[16]ESbuild 處理 TypeScript: https://esbuild.github.io/content-types/#typescript
[17]Vite Docs: https://cn.vitejs.dev/guide/features.html#typescript