背景
當使用 TypeScript + TSlint + Babel + Jest 搭建開發環境時,在開發過程中偶爾會被 IDE 提示「無法重新聲明塊范圍變量」,從而導致編譯出錯,報錯圖示如下:
相關開發環境配置如下:
- typescript: ^3.5.3
- tslint: ^5.19.0
- babel: ^7.0.0
- jest: ^24.9.0
- ts-jest: ^24.0.2
解決方案
之所以 tslint 會提示這個錯誤,是因為在 Commonjs 規范里,沒有像 ESModule 能形成閉包的「模塊」概念,所有的模塊在引用時都默認被拋至全局,因此當再次聲明某個模塊時,TypeScript 會認為重復聲明了兩次相同的變量進而拋錯。
對於這個問題,最簡單的解決方法是在報錯的文件底部添加一行代碼:export {}
。這行代碼會「欺騙」tslint 使其認為當前文件是一個 ESModule 模塊,因此不存在變量重復聲明的可能性。當使用這個方法時,記得這樣配置你的 tsconfig.json
文件:
{
"include": ["src", "demo"],
"compilerOptions": {
"module": "commonjs",
"noImplicitReturns": true,
"noUnusedLocals": true,
"esModuleInterop": true, // important!
"target": "esnext",
"strict": true,
"outDir": "app",
"declaration": true,
"sourceMap": true
}
}
其中 esMoudleInterop
這個配置允許文件中出現 export
關鍵字。
問題 2
當你以為已經萬事大吉的時候,你會發現第二個問題又浮出水面:你無法執行編譯后的 JavaScript 代碼!是的,因為 babel 雖然能夠幫你成功轉譯了 TypeScript 代碼,但是並沒有幫你去掉,你 hack 上的 export
關鍵字,因此 node 會由於無法識別該關鍵字而報錯。
此時你有兩個選擇:
刪除
export
關鍵字,忍受惱人的 IDE 提示,強行讓 babel 編譯;
幸運的是,這樣做真的行得通,因為 tslint 只是一個 “lint”,它只負責提示你哪里有問題,你可以強行忽略它。但是,當你使用 Jest 配合 TypeScript 進行測試時這樣做就行不通了,因為 Jest 會把 tslint 發現的錯誤當成無法原諒的錯誤告訴你,這意味着,你別想開開心心的測試你的代碼,當然你還可以選擇第二種解法:
寫一個 babel 插件,讓 babel 轉譯時去除
export
關鍵字;
這樣你的 node 可以識別轉譯后的代碼,你的 Jest 也不再會抱怨什么,兩全其美!然而,你真的想要專門為此寫一個插件嗎?
如果你的第一反應和我一樣是腦海中一個大大的 「NO!!!」,你應該繼續往下看了,其實我們還有第三個方案:)
終極解決方案
實際上,已經有一個 babel 插件可以滿足我們得需求了:@babel/plugin-transform-modules-commonjs
,這就是我們一直夢寐以求的東西。正如插件名所暗示的,它可以將 ESModule 模塊轉換為符合 Commonjs 規范的代碼,而經過我的測試,當遇到 export {}
這樣的表達式時,其轉譯的方案是:「直接忽略」!這正是我們想要的效果!
就這樣,在你的 babel.config.js
中加入這個插件,TSlint 不會再抱怨什么,Jest 能夠乖乖測試,Node 也不會朝你大吼 "What the * export !!",整個世界都清凈了。
最后,再分享一下我的全套相關配置,希望你們不再為這個問題感到困擾 😉:
tsconfig.json
{
"include": ["src", "demo"],
"compilerOptions": {
"module": "commonjs",
"noImplicitReturns": true,
"noUnusedLocals": true,
"esModuleInterop": true,
"target": "esnext",
"strict": true,
"outDir": "app",
"declaration": true,
"sourceMap": true
}
}
jest.config.js
module.exports = {
roots: ['<rootDir>/src'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
testPathIgnorePatterns: ['/node_moudles/', './src/utils/test.ts'],
}
babel.config.js
module.exports = {
presets: ['@babel/typescript'],
plugins: [
'@babel/plugin-transform-modules-commonjs',
'@babel/proposal-class-properties',
'@babel/proposal-object-rest-spread',
],
}