# 程式碼檢查

目前 TypeScript 的程式碼檢查主要有兩個方案：使用 [TSLint](https://palantir.github.io/tslint/) 或使用 [ESLint](https://eslint.org/) + [`typescript-eslint-parser`](https://github.com/eslint/typescript-eslint-parser)。

## 什麼是程式碼檢查

程式碼檢查主要是用來發現程式碼錯誤、統一程式碼風格。

在 JavaScript 專案中，我們一般使用 [ESLint](https://eslint.org/) 來進行程式碼檢查。它透過外掛化的特性極大的豐富了適用範圍，搭配 [`typescript-eslint-parser`](https://github.com/eslint/typescript-eslint-parser) 之後，甚至可以用來檢查 TypeScript 程式碼。

[TSLint](https://palantir.github.io/tslint/) 與 [ESLint](https://eslint.org/) 類似，不過除了能檢查常規的 js 程式碼風格之外，TSLint 還能夠透過 TypeScript 的語法解析，利用型別系統做一些 ESLint 做不到的檢查。

## 為什麼需要程式碼檢查

有人會覺得，JavaScript 非常靈活，所以需要程式碼檢查。而 TypeScript 已經能夠在編譯階段檢查出很多問題了，為什麼還需要程式碼檢查呢？

因為 TypeScript 關注的重心是型別的匹配，而不是程式碼風格。當團隊的人員越來越多時，同樣的邏輯不同的人寫出來可能會有很大的區別：

* 縮排應該是四個空格還是兩個空格？
* 是否應該禁用 `var`？
* 介面名是否應該以 `I` 開頭？
* 是否應該強制使用 `===` 而不是 `==`？

這些問題 TypeScript 不會關注，但是卻影響到多人協作開發時的效率、程式碼的可理解性以及可維護性。

下面來看一個具體的例子：

```typescript
let myName = 'Tom';

console.log(`My name is ${myNane}`);
console.log(`My name is ${myName.toStrng()}`);
console.log(`My name is ${myName}`)

// tsc 報錯資訊：
//
// index.ts(3,27): error TS2552: Cannot find name 'myNane'. Did you mean 'myName'?
// index.ts(4,34): error TS2551: Property 'toStrng' does not exist on type 'string'. Did you mean 'toString'?
//
//
//
// eslint 報錯資訊：
//
// /path/to/index.ts
//   3:27  error  'myNane' is not defined         no-undef
//   5:38  error  Missing semicolon               semi
//
// ✖ 2 problems (2 errors, 0 warnings)
//   1 errors, 0 warnings potentially fixable with the `--fix` option.
//
//
//
// tslint 報錯資訊：
//
// ERROR: /path/to/index.ts[5, 36]: Missing semicolon
```

| 存在的問題                      | `tsc` 是否報錯 | `eslint` 是否報錯 | `tslint` 是否報錯 |
| -------------------------- | ---------- | ------------- | ------------- |
| `myName` 被誤寫成了 `myNane`    | ✅          | ✅             | ❌             |
| `toString` 被誤寫成了 `toStrng` | ✅️         | ❌             | ❌             |
| 少了一個分號                     | ❌          | ✅             | ✅             |

上例中，由於 `eslint` 和 `tslint` 均無法識別 `myName` 存在哪些方法，所以對於拼寫錯誤的 `toString` 沒有檢查出來。

而程式碼風格的錯誤不影響編譯，故少了一個分號的錯誤 `tsc` 沒有檢查出來。

對於未定義的變數 `myNane`，`tsc` 可以檢測出來。由於用到 `tslint` 的地方肯定會接入 `tsc` 編譯，所以 `tslint` 就沒必要檢測這個錯誤了。`eslint` 需要能夠獨立於某個編譯環境執行，所以能檢測出此類別錯誤，而對於 TypeScript 程式碼，這其實是一種冗餘的檢測了。

事實上，不止 `tsc` 與 `eslint` 之間有冗餘的檢測，`tsc` 與 `tslint` 之間也有一些冗餘的檢測，但是大部分都是因為早期的 `tsc` 還沒能做到檢測此類別錯誤。

舉個例子，TSLint 中的 `typeof-compare` 要求 `typeof` 表示式比較的物件必須是 `'undefined'`, `'object'`, `'boolean'`, `'number'`, `'string'`, `'function'` 或 `'symbol'` 之一。而 TypeScript 2.2 之後，編譯器就已經自帶了這個功能。

下圖表示了 `tsc`, `eslint` 和 `tslint` 能覆蓋的檢查：

![TypeScript vs ESLint vs TSLint](https://2969600822-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LpS2d1WJqUy6hvq1AhT%2F-LpS2xMx34qtwxKcxCa8%2F-LpS2yabfY_E3NtjPsAI%2Ftypescript-eslint-tslint.png?generation=1569224395092587\&alt=media)

上圖中，`tsc`, `eslint` 和 `tslint` 之間互相都有重疊的部分，也有各自獨立的部分。

雖然發現程式碼錯誤比統一的程式碼風格更重要，但是當一個專案越來越龐大，開發人員也越來越多的時候，程式碼風格的約束還是必不可少的。

## 應該使用哪種程式碼檢查工具

TSLint 與 ESLint 作為檢查 TypeScript 程式碼的工具，各自有各自的優點：

TSLint 的優點：

1. 專為 TypeScript 服務，bug 比 ESLint 少
2. 不受限於 ESLint 使用的語法樹 [ESTree](https://github.com/estree/estree)
3. 能直接透過 `tsconfig.json` 中的配置編譯整個專案，使得在一個檔案中的型別定義能夠聯動到其他檔案中的程式碼檢查

ESLint 的優點：

1. 基礎規則比 TSLint 多很多（249 : 151）
2. 社群繁榮，外掛眾多（[50+](https://github.com/dustinspecker/awesome-eslint#plugins) : 9）

下面來看一些具體的例子：

```typescript
let foo: string = 1 + '1';

// tslint 報錯資訊：
//
// ERROR: /path/to/index.ts[1, 19]: Operands of '+' operation must either be both strings or both numbers, consider using template literals
```

以上程式碼在 TSLint 中會報錯，原因是加號兩邊必須同為數字或同為字串（需要開啟 `restrict-plus-operands` 規則）。

ESLint 無法知道加號兩邊的型別，所以對這種規則無能為力。

```typescript
function foo(a, b, c, d, e, f, g, h) {
    doSomething();
}

// eslint 報錯資訊：
//
// /path/to/index.ts
//   1:1  error  Function 'foo' has too many parameters (8). Maximum allowed is 7  max-params
//
// ✖ 1 problem (1 error, 0 warnings)
```

ESLint 可以檢測出來以上程式碼的函式引數超過了 7 個（需要開啟 `max-params` 規則）。

但是 TSLint 沒有此項檢查，雖然也可以實現，但是需要自己手動寫一條規則。

那麼到底該使用哪種程式碼檢測工具呢？經過一些實踐，我建議可以按照以下流程決定：

![流程圖：選擇 ESLint 還是 TSLint](https://2969600822-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LpS2d1WJqUy6hvq1AhT%2F-LpS2xMx34qtwxKcxCa8%2F-LpS2yaft4vM56fzW8Mq%2Feslint-or-tslint-process.png?generation=1569224395169300\&alt=media)

## 在 TypeScript 中使用 ESLint

### 安裝 ESLint

ESLint 可以安裝在當前專案中或全域性環境下，因為程式碼檢查是專案的重要組成部分，所以我們一般會將它安裝在當前專案中。可以執行下面的指令碼來安裝：

```bash
npm install eslint --save-dev
```

由於 ESLint 預設使用 [Espree](https://github.com/eslint/espree) 進行語法解析，無法識別 TypeScript 的一些語法，故我們需要安裝 `typescript-eslint-parser`，替代掉預設的解析器，別忘了同時安裝 `typescript`：

```bash
npm install typescript typescript-eslint-parser --save-dev
```

由於 `typescript-eslint-parser` 對一部分 ESLint 規則支援性不好，故我們需要安裝 `eslint-plugin-typescript`，彌補一些支援性不好的規則。

```bash
npm install eslint-plugin-typescript --save-dev
```

### 建立配置檔案

ESLint 需要一個配置檔案來決定對哪些規則進行檢查，配置檔案的名稱一般是 `.eslintrc.js` 或 `.eslintrc.json`。

當執行 ESLint 的時候檢查一個檔案的時候，它會首先嚐試讀取該檔案的目錄下的配置檔案，然後再一級一級往上查詢，將所找到的配置合併起來，作為當前被檢查檔案的配置。

我們在專案的根目錄下建立一個 `.eslintrc.js`，內容如下：

```javascript
module.exports = {
    parser: 'typescript-eslint-parser',
    plugins: [
        'typescript'
    ],
    rules: {
        // @fixable 必須使用 === 或 !==，禁止使用 == 或 !=，與 null 比較時除外
        'eqeqeq': [
            'error',
            'always',
            {
                null: 'ignore'
            }
        ],
        // 類別和介面的命名必須遵守帕斯卡命名法，比如 PersianCat
        'typescript/class-name-casing': 'error'
    }
}
```

以上配置中，我們指定了兩個規則，其中 `eqeqeq` 是 ESLint 原生的規則（它要求必須使用 `===` 或 `!==`，禁止使用 `==` 或 `!=`，與 `null` 比較時除外），`typescript/class-name-casing` 是 `eslint-plugin-typescript` 為 ESLint 增加的規則（它要求類別和介面的命名必須遵守帕斯卡命名法，比如 `PersianCat`）。

規則的取值一般是一個數組（上例中的 `eqeqeq`），其中第一項是 `off`、`warn` 或 `error` 中的一個，表示關閉、警告和報錯。後面的項都是該規則的其他配置。

如果沒有其他配置的話，則可以將規則的取值簡寫為陣列中的第一項（上例中的 `typescript/class-name-casing`）。

關閉、警告和報錯的含義如下：

* 關閉：禁用此規則
* 警告：程式碼檢查時輸出錯誤資訊，但是不會影響到 exit code
* 報錯：發現錯誤時，不僅會輸出錯誤資訊，而且 exit code 將被設為 1（一般 exit code 不為 0 則表示執行出現錯誤）

### 檢查一個 ts 檔案

建立了配置檔案之後，我們來建立一個 ts 檔案看看是否能用 ESLint 去檢查它了。

建立一個新檔案 `index.ts`，將以下內容複製進去：

```typescript
interface person {
    name: string;
    age: number;
}

let tom: person = {
    name: 'Tom',
    age: 25
};

if (tom.age == 25) {
    console.log(tom.name + 'is 25 years old.');
}
```

然後執行以下命令：

```bash
./node_modules/.bin/eslint index.ts
```

則會得到如下報錯資訊：

```bash
/path/to/index.ts
   1:11  error  Interface 'person' must be PascalCased  typescript/class-name-casing
  11:13  error  Expected '===' and instead saw '=='     eqeqeq

✖ 2 problems (2 errors, 0 warnings)
```

上面的結果顯示，剛剛配置的兩個規則都生效了：介面 `person` 必須寫成帕斯卡命名規範，`==` 必須寫成 `===`。

需要注意的是，我們使用的是 `./node_modules/.bin/eslint`，而不是全域性的 `eslint` 指令碼，這是因為程式碼檢查是專案的重要組成部分，所以我們一般會將它安裝在當前專案中。

可是每次執行這麼長一段指令碼頗有不便，我們可以透過在 `package.json` 中新增一個 `script` 來建立一個 npm script 來簡化這個步驟：

```javascript
{
    "scripts": {
        "eslint": "eslint index.ts"
    }
}
```

這時只需執行 `npm run eslint` 即可。

### 檢查整個專案的 ts 檔案

我們的專案原始檔一般是放在 `src` 目錄下，所以需要將 `package.json` 中的 `eslint` 指令碼改為對一個目錄進行檢查。由於 `eslint` 預設不會檢查 `.ts` 字尾的檔案，所以需要加上引數 `--ext .ts`：

```javascript
{
    "scripts": {
        "eslint": "eslint src --ext .ts"
    }
}
```

此時執行 `npm run eslint` 即會檢查 `src` 目錄下的所有 `.ts` 字尾的檔案。

### 在 VSCode 中整合 ESLint 檢查

在編輯器中整合 ESLint 檢查，可以在開發過程中就發現錯誤，極大的增加了開發效率。

要在 VSCode 中整合 ESLint 檢查，我們需要先安裝 ESLint 外掛，點選「擴充套件」按鈕，搜尋 ESLint，然後安裝即可。

VSCode 中的 ESLint 外掛預設是不會檢查 `.ts` 字尾的，需要在「檔案 => 首選項 => 設定」中，新增以下配置：

```javascript
{
    "eslint.validate": [
        "javascript",
        "javascriptreact",
        "typescript"
    ]
}
```

這時再開啟一個 `.ts` 檔案，將滑鼠移到紅色提示處，即可看到這樣的報錯資訊了：

![VSCode ESLint 錯誤資訊](https://2969600822-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LpS2d1WJqUy6hvq1AhT%2F-LpS2xMx34qtwxKcxCa8%2F-LpS2yakx399Y6rQZ9V1%2Fvscode-eslint-error.png?generation=1569224395084269\&alt=media)

### 使用 AlloyTeam 的 ESLint 配置

ESLint 原生的規則和 `eslint-plugin-typescript` 的規則太多了，而且原生的規則有一些在 TypeScript 中支援的不好，需要禁用掉。

這裡我推薦使用 [AlloyTeam ESLint 規則中的 TypeScript 版本](https://github.com/AlloyTeam/eslint-config-alloy#typescript)，它已經為我們提供了一套完善的配置規則。

安裝：

```bash
npm install --save-dev eslint typescript typescript-eslint-parser eslint-plugin-typescript eslint-config-alloy
```

在你的專案根目錄下建立 `.eslintrc.js`，並將以下內容複製到檔案中：

```javascript
module.exports = {
    extends: [
        'eslint-config-alloy/typescript',
    ],
    globals: {
        // 這裡填入你的專案需要的全域性變數
        // 這裡值為 false 表示這個全域性變數不允許被重新賦值，比如：
        //
        // jQuery: false,
        // $: false
    },
    rules: {
        // 這裡填入你的專案需要的個性化配置，比如：
        //
        // // @fixable 一個縮排必須用兩個空格替代
        // 'indent': [
        //     'error',
        //     2,
        //     {
        //         SwitchCase: 1,
        //         flatTernaryExpressions: true
        //     }
        // ]
    }
};
```

### 使用 ESLint 檢查 tsx 檔案

如果需要同時支援對 tsx 檔案的檢查，則需要對以上步驟做一些調整：

#### 安裝 `eslint-plugin-react`

```bash
npm install --save-dev eslint-plugin-react
```

#### package.json 中的 scripts.eslint 新增 `.tsx` 字尾

```javascript
{
    "scripts": {
        "eslint": "eslint src --ext .ts,.tsx"
    }
}
```

#### VSCode 的配置中新增 typescriptreact 檢查

```javascript
{
    "eslint.validate": [
        "javascript",
        "javascriptreact",
        "typescript",
        "typescriptreact"
    ]
}
```

#### 使用 AlloyTeam ESLint 規則中的 TypeScript React 版本

[AlloyTeam ESLint 規則中的 TypeScript React 版本](https://github.com/AlloyTeam/eslint-config-alloy#typescript-react)

## 在 TypeScript 中使用 TSLint

TSLint 的使用比較簡單，參考[官網的步驟](https://palantir.github.io/tslint/)安裝到本地即可：

```bash
npm install --save-dev tslint
```

建立配置檔案 `tslint.json`

```javascript
{
    "rules": {
        // 必須使用 === 或 !==，禁止使用 == 或 !=，與 null 比較時除外
        "triple-equals": [
            true,
            "allow-null-check"
        ]
    },
    "linterOptions": {
        "exclude": [
            "**/node_modules/**"
        ]
    }
}
```

為 `package.json` 新增 `tslint` 指令碼

```javascript
{
    "scripts": {
        "tslint": "tslint --project . src/**/*.ts src/**/*.tsx",
    }
}
```

其中 `--project .` 會要求 `tslint` 使用當前目錄的 `tsconfig.json` 配置來獲取型別資訊，很多規則需要型別資訊才能生效。

此時執行 `npm run tslint` 即可檢查整個專案。

### 在 VSCode 中整合 TSLint 檢查

在 VSCode 中安裝 `tslint` 外掛即可，安裝好之後，預設是開啟的狀態。

### 使用 AlloyTeam 的 TSLint 配置

AlloyTeam 為 TSLint 也打造了一套配置 [tslint-config-alloy](https://github.com/AlloyTeam/tslint-config-alloy)

```bash
npm install --save-dev tslint-config-alloy
```

安裝之後修改 `tslint.json` 即可

```javascript
{
    "extends": "tslint-config-alloy",
    "rules": {
        // 這裡填入你的專案需要的個性化配置，比如：
        //
        // 一個縮排必須用兩個空格替代
        // "indent": [
        //     true,
        //     "spaces",
        //     2
        // ]
    },
    "linterOptions": {
        "exclude": [
            "**/node_modules/**"
        ]
    }
}
```

### 使用 TSLint 檢查 tsx 檔案

TSLint 預設支援對 tsx 檔案的檢查，不需要做額外配置。

## Troubleshootings

### Cannot find module 'typescript-eslint-parser'

你執行的是全域性的 eslint，需要改為執行 `./node_modules/.bin/eslint`。

### cannot read property type of null

需要關閉 `eslint-plugin-react` 中的規則 `react/jsx-indent`。

如果仍然報錯，多半是因為某些規則需要被關閉，可以使用「二分排錯法」檢查是哪個規則造成了錯誤。也歡迎[給 eslint-config-alloy 提 issue](https://github.com/AlloyTeam/eslint-config-alloy/issues/new)。

### VSCode 沒有顯示出 ESLint 的報錯

1. 檢查「檔案 => 首選項 => 設定」中有沒有配置正確
2. 檢查必要的 npm 包有沒有安裝
3. 檢查 `.eslintrc.js` 有沒有配置
4. 檢查檔案是不是在 `.eslintignore` 中

如果以上步驟都不奏效，則可以在「檔案 => 首選項 => 設定」中配置 `"eslint.trace.server": "messages"`，按 `Ctrl`+`Shift`+`U` 開啟輸出面板，然後選擇 ESLint 輸出，檢視具體錯誤。

![VSCode 的 ESLint 輸出](https://2969600822-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LpS2d1WJqUy6hvq1AhT%2F-LpS2xMx34qtwxKcxCa8%2F-LpS2yaqLP89BXMTtKYN%2Fvscode-output-eslint.png?generation=1569224395027950\&alt=media)

### 為什麼 ESLint 無法檢查出使用了未定義的變數（`no-undef` 規則為什麼被關閉了）？

因為 `typescript-eslint-parser` [無法支援 `no-undef` 規則](https://github.com/eslint/typescript-eslint-parser/issues/416)。它針對正確的介面定義會報錯。

所以我們一般會關閉 `no-undef` 規則。

### 為什麼有些定義了的變數（比如使用 `enum` 定義的變數）未使用，ESLint 卻沒有報錯？

因為無法支援這種變數定義的檢查。建議在 `tsconfig.json` 中新增以下配置，使 `tsc` 編譯過程能夠檢查出定義了未使用的變數：

```javascript
{
    "compilerOptions": {
        "noUnusedLocals": true,
        "noUnusedParameters": true
    }
}
```

### 啟用了 noUnusedParameters 之後，只使用了第二個引數，但是又必須傳入第一個引數，這就會報錯了

第一個引數以下劃線開頭即可，參考 <https://github.com/Microsoft/TypeScript/issues/9458>

### 為什麼有的錯誤 TSLint 可以檢查出來，vscode 裡的 TSLint 卻檢查不出來？

因為 TSLint 依賴 `tsconfig.json` 獲得了型別資訊，而 [vscode 裡的 TSLint 暫不支援獲取型別資訊](https://github.com/Microsoft/vscode-tslint/tree/master/tslint#the-tslint-no-unused-variable-rule-doesnt-report-warnings-any-more)，所以 `no-unused-variable` 就失效了。

不僅 `no-unused-variables` 失效了，[TSLint rules](https://palantir.github.io/tslint/rules/) 裡面所有標有 `Requires Type Info` 的規則都失效了。

* [上一章：工程](https://willh.gitbook.io/typescript-tutorial/engineering)
* [下一章：感謝](https://willh.gitbook.io/typescript-tutorial/thanks)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://willh.gitbook.io/typescript-tutorial/engineering/lint.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
