跳至主要内容

[TS] Setup TypeScript Template

請到:pjchender/typescript-template @ pjchender github

Usage

npm start  # run src/index.ts

npx ts-node src/foobar.ts # run TS file directly

TL;DR

  • TypeScript 是使用 tsc 來編譯 TypeScript 檔案,會吃到的設定檔是 tsconfig.json。當在 VSCode 中看到 ts 的錯誤時,就是由 tsc 產生。
  • tsconfig.eslint.json 是給 @typescript-eslint 吃的 tsconfig 檔,它會繼承 tsconfig.json。當在 VSCode 中出現 @typescript-eslint 的錯誤就是由它產生的。
  • 出現 Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser. 的錯誤時,表示 ESLint 想要解析的檔案並沒有被列在給 @typescript-eslint 所涵括到的檔案中,解決方式可以參考 I get errors telling me "The file must be included in at least one of the projects provided" @ typescript-eslint

Project Implementation

Make TS Setup @ Github

添加 gitignore

直接使用 npx gitignore node 來添加給 NodeJS 使用的 gitignore 到專案中:

git init
npx gitignore node

安裝套件

yarn init --yes
yarn add -D typescript eslint jest

定義 package.json

// package.json
{
"types": "dist/index.ts", // 讓使用此套件的人知道 Type 的定義檔在哪
"scripts": {
"build": "tsc",
"dev": "yarn build --watch --preserveWatchOutput" // preserveWatchOutput 可以避免檔案變更時 console 中的內容被清空,
}
}

設定 tsconfig

yarn tsc --int

修改 tsconfig.json 中的內容:

{
"compilerOptions": {
"target": "ES2018", // 打包後要支援到的 ECMAScript 版本

// 如果沒設定會套用預設值(TS 自動判斷)
// 有「機會」導致 src 的資料夾也一起被 build 到 dist 資料夾中
// 例如 TS 自動把 rootDir 判斷為 { "rootDir": "." }
"rootDir": "src",

// 預設 tsc 會直接把編譯好的 js 檔放在與 ts 檔相同的路徑,但這樣檔案會很散亂,因此全部放到 dist
"outDir": "dist",

"stripInternal": true, // 針對加上 @internal 的方法不會產生對應的 Type Declaration 檔(例如針對測試寫的函式)
"types": [], // 在編譯時要一起包含的 declaration 檔

/* 下面這兩個項目之所以關閉,是因為一但開啟,其他使用本套件的開發者也需要被迫開啟 */
"esModuleInterop": false,
"skipLibCheck": false
},
"include": ["src"]
}

設定 ESLint

yarn eslint --init
  • How would you like to use ESLint? --> To check syntax and find problems(根據個人選擇)
  • What type of modules does your project use --> None of these(因為我們用的是 TS 而非 JS Modules)
  • Which framework does your project use? --> None of these
  • Does your project use TypeScript? --> Yes
  • Where does your code run? --> Node
  • What format do you want your config file to be in? --> JS

設定 @typescript-eslint

由於 @typescript-eslint 希望一個 TypeScript 設定檔就能涵括到所有想要 lint 的檔案,但因為測試時使用的環境會是不同的,所以我們必須額外建立一個檔案 tsconfig.eslint.json

tsc 編譯 TypeScript 時(和 VSCode 中看到的 ts 錯誤提示),實際上是根據 tsconfig.json 的設定;tsconfig.eslint.json 則是給 @typescript-eslint 用來解析型別用的(會繼承 tsconfig.json 中的設定),也就是 VSCode 中看到的 eslint 錯誤提示。

可以參考這兩篇:

另外可以參考 @typescript-eslint 提供的設定檔 .eslintrc.jstsconfig.eslint.json

設定 testing

方法一:@babel/preset-typescript

fix: use babel-typescript to run test instead of ts-jest

在這個範例中,TS 檔案測試的執行是使用 @babel/preset-typescript,而 TypeScript 本身的編譯器(tsc)並不會去執行測試的檔案。另外,當有寫 .babelrc 時,Jest 預設就會去吃這支檔案。

除了使用 @babel/preset-typescript 來執行測試外,另一種方式是使用 ts-jest

安裝對應的套件:

yarn add -D jest @types/jest @babel/preset-env @babel/preset-typescript
  • babel 是用來讓 jest 能夠解析 .ts 的檔案

  • 建立 .babelrc

    // .babelrc
    {
    "presets": [
    [
    "@babel/preset-env",
    {
    "targets": {
    "node": "14"
    }
    }
    ],
    "@babel/preset-typescript"
    ]
    }
  • 建立 tests/tsconfig.json(不建好像也可以)

透過 npx tsc --showConfig 可以檢視最終吃到的設定檔。

方法二:使用 ts-jest

Testing with jest in TypeScript @ medium

如果想要使用 ts-jest 的話,只需:

npm install ts-jest
npx ts-jest config:init

這時會新建一支 jest.config.js 的檔案,eslint 會試著去處理它,但它並沒有被包含在 tsconfig.eslint.json 中設定的路徑,因此又會出現 Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser. 此錯誤。

一樣只需要把該檔案放到 tsconfig.eslint.json 的 includes 中即可:

--- a/tsconfig.eslint.json
+++ b/tsconfig.eslint.json
@@ -8,6 +8,7 @@
"include": [
"src/**/*",
"tests/**/*",
- ".eslintrc.js"
+ ".eslintrc.js",
+ "jest.config.js"
]
}

ts-jest 是使用 tsc 來編譯 TypeScript 檔案而不是使用 @babel/preset-typescript,因此會吃的設定檔是 tsconfig.json,在原本 tsconfig.json 的設定中,需要進行如下修改:

--- a/tsconfig.json
+++ b/tsconfig.json
@@ -49,7 +49,7 @@
definitions from. */
- "types": [],
+ "types": ["jest"], // 讓 TS 認得 Jest
},
}

常見問題

設定 import alias / absolute import

tsconfig

TypeScript 可以透過以下設定來達到 import alias 的效果:

// tsconfig.json
{
// 將 @ 設為 src
"compilerOptions": {
"rootDir": "src",
"baseUrl": "." /* Base directory to resolve non-absolute module names. */,
"paths": {
"@/*": ["src/*"]
}
},

// 將 @ 設為「專案根目錄」
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}

eslint

除了 tsconfig 要設定外,因為有用 eslint 的關係,因此也要讓 import/resolver 認識設定好的 import alias,才不會 ESlint 一直報錯誤,這時候可以搭配 eslint-import-resolver-babel-module

$ npm install --save-dev eslint-import-resolver-babel-module babel-plugin-module-resolver

並且在 .eslintrc.js 中加上以下設定:

// .eslintrc.js
const allExtensions = ['.ts', '.tsx', '.d.ts', '.js', '.jsx', '.json'];
module.exports = {
// ...
settings: {
node: {
extensions: allExtensions,
},
+ // 將 @ 指向 src
+ 'import/resolver': {
+ 'babel-module': {
+ alias: {
+ '@': './src',
+ },
+ extensions: allExtensions,
+ },
+
+ // 將 @ 指向專案根目錄
+ 'import/resolver': {
+ 'babel-module': {
+ alias: {
+ '@': './',
+ },
+ extensions: allExtensions,
},
},
},
},
}

使用 tsconfig-paths 解決 ts-node 無法 resolve 的問題

如果我們直接使用 ts-node 來執行包含 import alias 的檔案時,它會無法解析,這時候需要 tsconfig-paths 的幫忙:

$ npm install --save-dev tsconfig-paths

接著在使用 ts-node 時加上 tsconfig-paths 即可:

$ npx ts-node -r tsconfig-paths/register src/index.ts

或者放到 package.json 中:

{
"scripts": {
"start": "ts-node -r tsconfig-paths/register src/index.ts"
}
}

支援用 node 執行編譯過的檔案(無法同時支援 ts-node)

當我們透過 node dist/index.js 直接執行打包好的 js 檔時會發上錯誤:

typescript import alias

這是因為透過 tsc 打包過的檔案並不會針對 import alias 進行處理(參考:Module path maps are not resolved in emitted code #10866):

import alias typescript

這時候會需要安裝可以透過 module-alias 這個套件:

$ npm i --save module-alias

package.json 中進行設定:

diff --git a/package.json b/package.json
index cec3cf4..fb55264 100644
--- a/package.json
+++ b/package.json
@@ -46,5 +46,11 @@
"*.{js,jsx,ts,tsx}": [
"eslint --fix"
]
+ },
+ "dependencies": {
+ "module-alias": "^2.2.2"
+ },
+ "_moduleAliases": {
+ "@": "dist"
}
}

接著在專案的 entry file(例如,main.tsindex.ts)載入這個套件:

diff --git a/src/index.ts b/src/index.ts
index 1a565ed..0f3ad68 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,3 +1,5 @@
+import 'module-alias/register';
+
import { example1, example2, example3 } from './examples/for-async-await';

function main() {

重新打包 ts 檔案後即可使用 node 執行:

$ tsc  # build ts files
$ node dist/index.js # run compiled files
warning

使用 module-alias 雖然可以讓 node 執行 dist 中打包好的檔案,但卻會讓 ts-node 無法正確執行,因此若想使用 node-ts 直接執行 index.ts 的話,需要把 import 'module-alias/register'; 拿掉。

該使用 TSC 或 Babel 編譯 TypeScript 檔

Using Babel with TypeScript @ TypeScript

一個簡單的原則是:

  • 如果你打包後的檔案和 source files 是很接近的,使用 tsc
  • 如果需要透過 build pipeline 產生編譯後不同的檔案(例如,.esm, cjs),那麼使用 babel 來編譯 TypeScript 檔,並透過 tsc 來做型別檢查。

使用 Babel 提供的 preset-typescript 來進行編譯的好處在於它可以和專案原本的 pipeline 結合,並且打包所需的時間較短,因為在編譯時 Babel 不會進行型別檢查。

但相對地,也因為 Babel 在編譯時不會進行型別檢查,型別檢查的過程完全是透過 Editor,因此如果你忽略了 Editor 上的 TS 錯誤,還是可以正常編譯,這可能會導致你忽略了型別上的錯誤。另一方面,Babel 也不會產生 .d.ts 的型別定義檔,若你開發的是套件,那麼使用你套件的開發者將無法取得對應的型別資訊。

為了要解決這樣的問題,最常見的方式就是「透過 Babel 來編譯 TypeScript 檔,並搭配 tsc 來進行型別檢查和產生 .d.ts」。為了達到這樣的效果,確保在 tsconfig 中的下述設定:

// tsconfig.json

"compilerOptions": {
// Ensure that .d.ts files are created by tsc, but not .js files
"declaration": true,
"emitDeclarationOnly": true,
// Ensure that Babel can safely transpile files in the TypeScript project
"isolatedModules": true
}

Reference