【長文詳解】TypeScript、Babel、webpack以及IDE對TS的型別檢查

2022-09-04 21:03:50

只要接觸過ts的前端同學都能回答出ts是js超集,它具備靜態型別分析,能夠根據型別在靜態程式碼的解析過程中對ts程式碼進行型別檢查,從而在保證型別的一致性。那,現在讓你對你的webpack專案(其實任意型別的專案都同理)加入ts,你知道怎麼做嗎?帶著這個問題,我們由淺入深,逐步介紹TypeScriptBabel以及我們日常使用IDE進行ts檔案型別檢查的關係,讓你今後面對基於ts的工程能夠做到遊刃有餘。

TypeScript基本認識

原則1:主流的瀏覽器的主流版本只認識js程式碼

原則2:ts的程式碼一定會經過編譯為js程式碼,才能執行在主流瀏覽器上

要編譯ts程式碼,至少具備以下幾個要素:

  1. ts原始碼
  2. ts編譯器
  3. ts編譯器所需要的設定(預設設定也是設定)

編譯TS的方式

目前主流的ts編譯方案有2種,分別是官方tsc編譯、babel+ts外掛編譯。

官方tsc編譯器

對於ts官方模式來說,ts編譯器就是tsc(安裝typescript就可以獲得),而編譯器所需的設定就是tsconfig.json組態檔形式或其他形式。ts原始碼經過tsc的編譯(Compile),就可以生成js程式碼,在tsc編譯的過程中,需要編譯設定來確定一些編譯過程中要處理的內容。

我們首先準備一個ts-demo,該demo中有如下的結構:

ts-demo
 |- packages.json
 |- tsconfig.json
 |- src
    |- index.ts

安裝typescript

yarn add -D typescript

package.json

{
  "name": "ts-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build-ts": "tsc"
  },
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "typescript": "^4.7.4"
  }
}

tsconfig.js(對於這個簡單的tsconfig,我不再贅述其設定的含義。)

{
  "compilerOptions": {
    "module": "commonjs",
    "rootDir": "./src",
    "outDir": "./dist"
  }
}

index.ts

interface User {
    name: string;
    age: number;
}
const userToString = (user: User) => `${user.name}@${user.age}`;
export {userToString, User};

此時,我們只需要執行yarn build-ts就可以將我們的index.ts編譯為index.js:

commonjs模組化方式產物:

"use strict";
exports.__esModule = true;
exports.userToString = void 0;
var userToString = function (user) { return "".concat(user.name, "@").concat(user.age); };
exports.userToString = userToString;

可以看到,原本index.ts編譯為index.js的產物,使用了commonjs模組化方案(tsconfig裡面設定模組化方案是"commonjs",編譯後的程式碼可以看到"exports"的身影);倘若我們將模組化方案改為ESM(ES模組化)的es:"module": "es6",編譯後的產物依然是index.js,只不過內容採用了es6中的模組方案。

es6模組化方式產物:

var userToString = function (user) {
  return "".concat(user.name, "@").concat(user.age);
};
export {userToString};

說了這麼多,只是想要告訴各位同學,ts無論有多麼龐大的語法體系,多麼強大的型別檢查,最終的產物都是js

此外,ts中的模組化,不能和js中的模組化混為一談。js中的模組化方案很多(es6、commonjs、umd等等),所以ts本身在編譯過程中,需要指定一種js的模組化表達,才能編譯為對應的程式碼。也就是說,在ts中的import/export,不能認為和es6的import/export是一樣的,他們是完全不同的兩個體系!只是語法上類似而已。

babel+ts外掛

如前文所述

ts原始碼經過tsc的編譯(Compile),就可以生成js程式碼,在tsc編譯的過程中,需要編譯設定來確定一些編譯過程中要處理的內容。

那麼是不是說,編譯器這塊是不是有其他的代替呢?ts原始碼經過某種其他的編譯器編譯後,生成目標js程式碼。答案是肯定的:babel。

我們準備一個ts-babel-demo:

ts-babel-demo
 |- packages.json
 |- .babelrc
 |- src
    |- index.ts

依賴新增:

 yarn add -D @babel/core @babel/cli
 yarn add -D @babel/preset-env @babel/preset-typescript
 yarn add -D @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread

package.json:

{
  "name": "ts-babel-demo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "private": true,
  "scripts": {
    "build": "babel src -d dist -x '.ts, .tsx'"
  },
  "devDependencies": {
    "@babel/cli": "^7.18.10",
    "@babel/core": "^7.18.10",
    "@babel/plugin-proposal-class-properties": "^7.18.6",
    "@babel/plugin-proposal-object-rest-spread": "^7.18.9",
    "@babel/preset-env": "^7.18.10",
    "@babel/preset-typescript": "^7.18.6"
  }
}

.babelrc

{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-typescript"
  ],
  "plugins": [
    "@babel/plugin-proposal-object-rest-spread",
    "@babel/plugin-proposal-class-properties"
  ]
}

index.ts和ts-demo保持一致。

完成基礎的專案搭建以後,我們執行yarn build

~/Projects/web-projects/ts-babel-demo > yarn build
yarn run v1.22.17
$ babel src -d dist -x '.ts, .tsx'
Successfully compiled 1 file with Babel (599ms).
Done in 4.05s.

可以看到專案dist目錄下出現了編譯好的js程式碼:

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.userToString = void 0;

var userToString = function userToString(user) {
  return "".concat(user.name, "@").concat(user.age);
};

exports.userToString = userToString;

可以看到和使用tsc編譯為commonjs效果是一樣。

回顧這個專案,其實按照我們之前的思路來梳理:

  1. ts原始檔(src/index.ts)
  2. ts的編譯器(babel)
  3. 編譯設定(.babelrc)

瞭解babel機制

如果對於babel不太熟悉,可能對上述的一堆依賴感到恐懼:

 yarn add -D @babel/core @babel/cli
 yarn add -D @babel/preset-env @babel/preset-typescript
 yarn add -D @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread

這裡如果讀者有時間,我推薦這篇深入瞭解babel的文章:一口(很長的)氣了解 babel - 知乎 (zhihu.com)。當然,如果這口氣憋不住(哈哈),我做一個簡單摘抄:

babel 總共分為三個階段:解析,轉換,生成。

babel 本身不具有任何轉化功能,它把轉化的功能都分解到一個個 plugin 裡面。因此當我們不設定任何外掛時,經過 babel 的程式碼和輸入是相同的。

外掛總共分為兩種:

  • 當我們新增 語法外掛 之後,在解析這一步就使得 babel 能夠解析更多的語法。(順帶一提,babel 內部使用的解析類庫叫做 babylon,並非 babel 自行開發)

舉個簡單的例子,當我們定義或者呼叫方法時,最後一個引數之後是不允許增加逗號的,如 callFoo(param1, param2,) 就是非法的。如果原始碼是這種寫法,經過 babel 之後就會提示語法錯誤。

但最近的 JS 提案中已經允許了這種新的寫法(讓程式碼 diff 更加清晰)。為了避免 babel 報錯,就需要增加語法外掛 babel-plugin-syntax-trailing-function-commas

  • 當我們新增 轉譯外掛 之後,在轉換這一步把原始碼轉換並輸出。這也是我們使用 babel 最本質的需求。

比起語法外掛,轉譯外掛其實更好理解,比如箭頭函數 (a) => a 就會轉化為 function (a) {return a}。完成這個工作的外掛叫做 babel-plugin-transform-es2015-arrow-functions

同一類語法可能同時存在語法外掛版本和轉譯外掛版本。如果我們使用了轉譯外掛,就不用再使用語法外掛了。

簡單來講,使用babel就像如下流程:

原始碼 =babel=> 目的碼

如果沒有使用任何外掛,原始碼和目的碼就沒有任何差異。當我們引入各種外掛的時候,就像如下流程一樣:

原始碼
|
進入babel
|
babel外掛1處理程式碼:移除某些符號
|
babel外掛2處理程式碼:將形如() => {}的箭頭函數,轉換成function xxx() {}
|
目的碼

因為babel的外掛處理的力度很細,我們程式碼的語法、語意內容規範有很多,如果我們要處理這些語法,可能需要設定一大堆的外掛,所以babel提出,將一堆外掛組合成一個preset(預置外掛包),這樣,我們只需要引入一個外掛組合包,就能處理程式碼的各種語法、語意。

所以,回到我們上述的那些@babel開頭的npm包,再回首可能不會那麼迷茫:

@babel/core
@babel/preset-env
@babel/preset-typescript
@babel/preset-react
@babel/plugin-proposal-class-properties
@babel/plugin-proposal-object-rest-spread
  • @babel/core毋庸置疑,babel的核心模組,實現了上述的流程運轉以及程式碼語法、語意分析的功能;

  • @babel/cli則是我們可以在命令列使用babel命令;

  • plugin開頭的就是外掛,這裡我們引入了兩個:@babel/plugin-proposal-class-properties允許類具有屬性)和@babel/plugin-proposal-object-rest-spread物件展開);

  • preset開頭的就是預置元件包合集,其中@babel/preset-env表示使用了可以根據實際的瀏覽器執行環境,會選擇相關的跳脫外掛包,通過設定得知目標環境的特點只做必要的轉換。如果不寫任何設定項,env 等價於 latest,也等價於 es2015 + es2016 + es2017 三個相加(不包含 stage-x 中的外掛);@babel/preset-typescript會處理所有ts的程式碼的語法和語意規則,並轉換為js程式碼。

關於babel編譯ts,並不是所有的語法都支援,這裡有一篇文章專門介紹了其中注意點:《TypeScript 和 Babel:美麗的結合》。

webpack專案級TS使用

前面的內容,我們已經介紹了將ts編譯為js的兩種方式(tsc、babel),但僅僅是簡單將一個index.ts編譯為index.js。實際上,對於專案級別的ts專案,還有很多需要了解的。接下來基於一個webpack專案來逐步介紹如何基於前文的兩種方式來使用ts。

對於webpack來說,至少需要讀者瞭解到webpack的基本機制:概念 | webpack 中文檔案 (docschina.org)

簡單來講,webpack執行從指定的entry檔案開始,從頂層開始分析依賴的內容,依賴的內容可以是任何的內容(只要是import的或require了的),而loader可以專門來處理各種型別的檔案。

webpack 只能理解 JavaScript 和 JSON 檔案,這是 webpack 開箱可用的自帶能力。loader 讓 webpack 能夠去處理其他型別的檔案,並將它們轉換為有效 模組,以供應用程式使用,以及被新增到依賴圖中

所以,當一個webpack專案是基於TS進行的時候,我們一定會有一個loader來處理ts(甚至是tsx)。當然,我們還是通過demo搭建來演示講解。

ts-loader

mkdir webpack-ts-loader-demo && cd webpack-ts-loader-demo
yarn init
yarn add -D webpack webpack-cli
yarn add -D ts-loader

package.json

{
  "name": "webpack-ts-loader-demo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "webpack --config webpack.config.js"
  },
  "devDependencies": {
    "ts-loader": "^9.3.1",
    "webpack": "^5.74.0",
    "webpack-cli": "^4.10.0"
  }
}

webpack.config.js

const {resolve} = require('path');
module.exports = {
  entry: './src/index.ts',
  output: {
    path: resolve(__dirname, './dist'),
    filename: "index.js"
  },
  module: {
    rules: [
      {
        test: /\.ts/,
        loader: "ts-loader"
      }
    ]
  }
};

src/index.ts

interface User {
    name: string;
    age: number;
}
const userToString = (user: User) => `${user.name}@${user.age}`;
export {userToString, User};

表面上,只需要上述三個檔案,就可以編譯ts檔案,但是嘗試執行yarn build會報錯:

Module build failed (from ./node_modules/ts-loader/index.js):
Error: Could not load TypeScript. Try installing with `yarn add typescript` or `npm install typescript`. If TypeScript is installed globally, try using `yarn link typescript` or `npm link typescript`.

通過報錯很容易理解,我們沒有安裝typescript。為什麼?因為ts-loader本身處理ts檔案的時候,本質上還是呼叫的tsc,而tsc是typescript模組提供的。因此,我們只需要yarn add -D typescript即可(其實只需要開發依賴即可),但是緊接著又會有另外一個報錯:

ERROR in ./src/index.t
Module build failed (from ./node_modules/ts-loader/index.js):
Error: error while parsing tsconfig.json

報錯提醒我們,解析tsconfig的出錯,不難理解,我們還沒有設定tsconfig.json,因為tsc需要!所以,在我們專案中,加上tsconfig.json即可:

tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "rootDir": "./src",
    "outDir": "./dist"
  }
}

設定完成以後,我們再次編譯,發現可以編譯成功,並且在dist目錄下會有對應的js程式碼。

然而,事情到這裡就結束了嗎?一箇中大型的專案,必然有模組的引入,假如現在我們新增了個utils.ts

export const hello = () => {
  return 'hello';
}

修改index.ts的程式碼,引入該hello方法,並使用:

import {hello} from "./utils"; // 引入utils
interface User {
    name: string;
    age: number;
}
const userToString = (user: User) => `${user.name}@${user.age}${hello()}`;
export {userToString, User};

再次執行yarn build,讀者會發現還是會報錯,但這一次的錯誤略有點出乎意料:

Module not found: Error: Can't resolve './utils' in '/Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src'
resolve './utils' in '/Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src'

核心報錯在於,webpack似乎無法找到utils這個模組。為什麼呢?因為webpack預設是處理js程式碼的,如果你的程式碼中編寫了import xxx from 'xxx',在沒有明確指明這個模組的字尾的時候,webpack只會認為這個模組是以下幾種:

  1. 無字尾檔案
  2. js檔案
  3. json檔案
  4. wasm檔案

所以,你會看到具體一點的報錯:

resolve './utils' in '/Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src'
  using description file: /Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/package.json (relative path: ./src)
    Field 'browser' doesn't contain a valid alias configuration
    using description file: /Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/package.json (relative path: ./src/utils)
      no extension
        Field 'browser' doesn't contain a valid alias configuration
        /Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src/utils doesn't exist
      .js
        Field 'browser' doesn't contain a valid alias configuration
        /Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src/utils.js doesn't exist
      .json
        Field 'browser' doesn't contain a valid alias configuration
        /Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src/utils.json doesn't exist
      .wasm
        Field 'browser' doesn't contain a valid alias configuration
        /Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src/utils.wasm doesn't exist
      as directory

要想讓webpack知道我們引入的utils是ts程式碼,方式為在webpack設定中,指明webpack預設處理的檔案字尾:

const {resolve} = require('path');
module.exports = {
  // ... ...
  resolve: {
    // webpack 預設只處理js、jsx等js程式碼
    // 為了防止在import其他ts程式碼的時候,出現
    // " Can't resolve 'xxx' "的錯誤,需要特別設定
    extensions: ['.js', '.jsx', '.ts', '.tsx']
  },
  // ... ...
};

完成設定以後,我們就能夠正確編譯具備模組匯入的ts程式碼了。

綜合來看,在基於ts-loader的webpack專案的解析流程處理如下。

回顧一下webpack,它預設處理模組化js程式碼,比如index.js參照了utils.js(模組參照方式可以是commonjs,也可以是esModule形式),那麼webpack從入口的index.js出發,來處理依賴,並打包為一個js(暫不考慮js拆分)。

對於wepack+ts-loader的ts專案體系主要是通過ts-loader內部呼叫typescript提供的tsc,將ts程式碼編譯為js程式碼(編譯後的js程式碼依然是js模組化的形式),所以這個過程是需要tsconfig參與;等到tsc將整個所有的ts程式碼均編譯為js程式碼以後,再整體交給webpack進行依賴分析並打包(也就進入webpack的預設處理流程)。

細心的讀者會發現這個過程有一個問題:由於先經過tsc編譯後的js,又再被webpack預設的js處理機制進行分析並編譯打包,這個過程一方面經過了兩次編譯(ts->標準模組化js->webpack模組體系js),那麼如果ts專案特別大,模組特別多的時候,這個兩次編譯的過程會特別漫長!

babel-loader

前面我們簡單介紹瞭如何使用babel對一份ts進行編譯,那麼在webpack中,如何使用babel呢?有的同學可能會想到這樣操作步驟:我先用babel對ts進行編譯為js,然後再利用webpack對js進行打包,這樣的做法是可以的,但細想不就和上面的ts-loader一樣的情況了嗎?

只要開發過基於webpack的現代化前端專案的同學,或多或少都看到過babel-loader的身影,他是個什麼東西呢?先說結論吧,babel-loader是webpack和babel(由@babel/core和一堆預置集preset、外掛plugins組合)的橋樑。

根據這個圖,同學可能覺得這不是和ts-loader的架構很像嗎?webpack啟動,遇到入口ts,匹配到babel-loader,babel-loader交給babel處理,處理完畢,回到webpack打包。但是使用babel進行ts處理,比起ts-loader更加高效。而關於這塊的說明,我更加推薦讀者閱讀這篇文章 TypeScript 和 Babel:美麗的結合 - 知乎 (zhihu.com),簡單來講:

警告!有一個震驚的訊息,你可能想坐下來好好聽下。

Babel 如何處理 TypeScript 程式碼?它刪除它

是的,它刪除了所有 TypeScript,將其轉換為「常規的」 JavaScript,並繼續以它自己的方式愉快處理。

這聽起來很荒謬,但這種方法有兩個很大的優勢。

第一個優勢:️⚡️閃電般快速⚡️。

大多數 Typescript 開發人員在開發/監視模式下經歷過編譯時間長的問題。你正在編寫程式碼,儲存一個檔案,然後...它來了...再然後...最後,你看到了你的變更。哎呀,錯了一個字,修復,儲存,然後...啊。它只是慢得令人煩惱並打消你的勢頭。

很難去指責 TypeScript 編譯器,它在做很多工作。它在掃描那些包括 node_modules 在內的型別定義檔案(*.d.ts),並確保你的程式碼正確使用。這就是為什麼許多人將 Typescript 型別檢查分到一個單獨的程序。然而,Babel + TypeScript 組合仍然提供更快的編譯,這要歸功於 Babel 的高階快取和單檔案發射架構。

讓我們來搭建一個專案來複習這一過程吧:

mkdir webpack-babel-loader-demo && cd webpack-babel-loader-demo
yarn init
yarn add -D webpack webpack-cli
yarn add -D babel-loader
yarn add -D @babel/core
yarn add -D @babel/preset-env @babel/preset-typescript
yarn add -D @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread

package.json

{
  "name": "webpack-babel-loader-demo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "webpack --config webpack.config.js" 
  },
  "devDependencies": {
    "@babel/core": "^7.18.13",
    "@babel/plugin-proposal-class-properties": "^7.18.6",
    "@babel/plugin-proposal-object-rest-spread": "^7.18.9",
    "@babel/preset-env": "^7.18.10",
    "@babel/preset-typescript": "^7.18.6",
    "babel-loader": "^8.2.5",
    "webpack": "^5.74.0",
    "webpack-cli": "^4.10.0"
  }
}

webpack.config.js

const {resolve} = require('path');
module.exports = {
  entry: './src/index.ts',
  output: {
    path: resolve(__dirname, './dist'),
    filename: "index.js"
  },
  resolve: {
    // webpack 預設只處理js、jsx等js程式碼
    // 為了防止在import其他ts程式碼的時候,出現
    // " Can't resolve 'xxx' "的錯誤,需要特別設定
    extensions: ['.js', '.jsx', '.ts', '.tsx']
  },
  module: {
    rules: [
      {
        test: /\.ts/,
        loader: "babel-loader"
      }
    ]
  }
};

src/index.ts

import {hello} from "./utils";
interface User {
    name: string;
    age: number;
}
const userToString = (user: User) => `${user.name}@${user.age}${hello()}`;
export {userToString, User};

src/utils.ts

export const hello = () => {
  return 'hello';
}

完成上述package.json、webpack.config.js、src原始碼三個部分,我們可以開始執行yarn build,但實際上會報錯:

ERROR in ./src/index.ts
Module build failed (from ./node_modules/babel-loader/lib/index.js):
SyntaxError: /Users/w4ngzhen/Projects/web-projects/webpack-babel-loader-demo/src/index.ts: Unexpected reserved word 'interface'. (1:0)

> 1 | interface User {
    | ^
  2 |     name: string;
  3 |     age: number;
  4 | }
    at instantiate (/Users/w4ngzhen/Projects/web-projects/webpack-babel-loader-demo/node_modules/@babel/parser/lib/index.js:72:32)

出現了語法的錯誤,報錯的主要原因在於沒有把整個babel處理ts的鏈路打通。目前的鏈路是:webpack找到入口ts檔案,匹配上babel-loader,babel-loader交給@babel/core,@babel/core處理ts。由於我們沒有給@babel/core設定plugin、preset,所以導致了babel還是以預設的js角度來處理ts程式碼,所以有語法報錯。此時,我們需要新增.babelrc檔案來指明讓babel載入處理ts程式碼的外掛:

.babelrc

{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-typescript"
  ],
  "plugins": [
    "@babel/plugin-proposal-object-rest-spread",
    "@babel/plugin-proposal-class-properties"
  ]
}

完成設定以後,我們再次執行yarn build,編譯通過,但是在dist下的index.js卻是空白的!

問題:babel-loader編譯後,輸出js內容空白

如果按照上述的設定以後,我們能夠成功編譯但是卻發現,輸出的js程式碼是空白的!原因在於:我們編寫的js程式碼,是按照類庫的模式進行編寫(在indexjs中只有匯出一些函數卻沒有實際的使用),且webpack打包的時候,沒有指定js程式碼的編譯為什麼樣子的庫。

假如我們在index中編寫一段具有副作用的程式碼:

import {hello} from "./utils";
interface User {
    name: string;
    age: number;
}
const userToString = (user: User) => `${user.name}@${user.age}${hello()}`;

// 具備副作用:在id=app的元素上新增監聽
document
    .querySelector('#app')
    .addEventListener('click', () => {})

export {userToString, User};

此時我們使用生產模式(mode: 'production')來編譯,會發現dist/index.js的內容如下:

(() => {
  "use strict";
  document.querySelector("#app").addEventListener("click", (function () {
  }));
})();

會發現只有副作用程式碼,但是userToString相關的程式碼完全被剔除了!這時候,可能有讀者會說,我匯出的程式碼有可能別人會使用,你憑什麼要幫我剔除?其實,因為webpack預設是生成專案使用的js,也就是做打包操作,他的目的是生成當前專案需要的js。在我們這個範例中,在沒有寫副作用之前,webpack認為打包是沒有意義的,因為只有匯出方法,卻沒有使用。那麼,如果讓webpack知道,我們需要做一個類庫呢?在webpack中設定library欄位即可:

const {resolve} = require('path');
module.exports = {
  entry: './src/index.ts',
  mode: 'production',
  output: {
    // ... ...
    library: { 
      // 設定library欄位的相關設定,這裡我們設定為commonjs2
      // 至於這塊設定的意義,讀者需要自行學習~
      type: 'commonjs2',
    },
  },
  // ... ...
};

tsc與babel編譯的差異

現在我們先編寫一個簡單錯誤程式碼

interface User {
    name: string;
    age: number;
}
// user.myName並沒有在User介面中提供
const userToString = (user: User) => `${user.myName}@${user.age}`;
export {userToString, User};

在這個範例中,我們試圖存取在User型別中不存在的myName欄位。

ts-loader

前面我們提到了ts-loader內部呼叫的是tsc作為編譯器,我們嘗試執行基於ts-loader的webpack設定進行打包該模組,會發現報錯:

... ...
      TS2551: Property 'myName' does not exist on type 'User'. Did you mean 'name'?
ts-loader-default_e3b0c44298fc1c14

webpack 5.74.0 compiled with 1 error in 2665 ms
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

可以看得出來,tsc幫助我們提示了型別錯誤的地方,user這個型別並沒有對應的myName欄位。

babel-loader

我們切換一下到babel-loader對該ts檔案進行編譯,居然發現編譯可以直接成功!並且,我們檢查編譯好的js程式碼,會發現這部分:

// dist/index.js
(() => {
  "use strict";
  // ... ...
  var r = function (e) {
    // 注意這個地方:依然在使用myName
    return "".concat(e.myName, "@").concat(e.age);
  };
  module.exports = o;
})();

編譯好的js程式碼就在直接使用myName欄位。為什麼型別檢查失效了?還記得我們前面提到的babel怎麼處理ts的?

Babel 如何處理 TypeScript 程式碼?它刪除它

是的,它刪除了所有 TypeScript,將其轉換為「常規的」 JavaScript,並繼續以它自己的方式愉快處理。

是的,babel並沒有進行型別檢查,而是將各種型別移除掉以達到快速完成編譯的目的。那麼問題來了,我們如何讓babel進行型別判斷呢?實際上,我們沒有辦法讓babel進行型別判斷,必須要藉助另外的工具進行。那為什麼我們的IDE卻能夠現實ts程式碼的錯誤呢?因為IDE幫助我們進行了型別判斷。

主流IDE對TypeScript的型別檢查

不知道有沒有細心的讀者在使用IDEA的時候,發現一個ts專案的IDEA右下角展示了typescript:

VSCode也能看到類似:

在同一臺電腦上,甚至發現IDEA和VSCode的typescript版本都還不一樣(4.7.4和4.7.3)。這是怎麼一回事呢?實際上,IDE檢測到你所在的專案是一個ts專案的時候(或包含ts檔案),就會自動的啟動一個ts的檢測服務,專門用於所在專案的ts型別檢測。這個ts型別檢測服務,是通過每個IDE預設情況下自帶的typescript中的tsc進行型別檢測。

但是,我們可以全域性安裝(npm -g)或者是為每個專案單獨安裝typescript,然後就可以讓IDE選擇啟動獨立安裝的typescript。比如,我們在本專案中,安裝一個特定版本的ts(版本4.7.2):

yarn add -D [email protected]

在IDEA中,設定 - Languages & Frameworks - TypeScript中,就可以選擇IDEA啟動的4.7.2版本的TypeScript為我們專案提供型別檢查(注意看選項中有一個Bundled的TS,版本是4.7.4,就是預設的):

IDE之所以能夠在對應的程式碼位置展示程式碼的型別錯誤,流程如下:

但是,ts型別檢查也要有一定的依據。譬如,有些型別定義的檔案從哪裡查詢,是否允許較新的語法等,這些設定依然是由tsconfig.json來提供的,但若未提供,則IDE會使用一份預設的設定。如果要進行型別檢測的自定義設定,則需要提供tsconfig.json。

還記得我們前面的ts-loader嗎?在程式碼編譯期,ts-loader呼叫tsc,tsc讀取專案目錄下的tsconfig.json設定。而咱們編寫程式碼的時候,又讓IDE的ts讀取該tsconfig.json組態檔進行型別檢查。

對於ts-loader專案體系來說,ts程式碼編譯和ts的型別檢測如下:

然而,對於babel-loader專案體系就不像ts-loader那樣了:

在babel-loader體系中,程式碼的編譯只取決於babel部分的處理,根型別沒有根本的關係,而型別檢查使用到的tsconfig和tsc則只作用在型別檢查的部分,根ts程式碼編譯沒有任何關係。