tsconfig常用配置全解

基於typescript的項目的根目錄下都會有一個文件(tsconfig.json), 這個文件主要用來控制typescript編譯器(tsc, typescript compiler)的一些行為, 比如指定哪些文件需要讓tsc來編譯, 哪些文件不想讓tsc進行編譯之類的.

angular項目的tsconfig.json文件

tsconfig.json
/* To learn more about this file see: //angular.io/config/tsconfig. */
{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "es2015",
    "module": "es2020",
    "lib": [
      "es2018",
      "dom"
    ]
  },
  "angularCompilerOptions": {
    "enableI18nLegacyMessageIdFormat": false,
    "strictTemplates": true
  }
}

這其中angularCompilerOptions顧名思義是angular專用的, 不在本文討論範圍.

include, exclude, files配置項

include: 指定要編譯哪些文件, 比如只需要編譯<project-dir>/src目錄下的.ts源程式碼文件

{
    "compilerOptions": {
        ...
    },
    include: ["./src/**/*", "./demo/**/*.tsx?"]
}

上面的include配置用到了兩個通配符: **/, *

**/表示匹配任何子路徑, 包括目錄分隔符/也會被它匹配, 所以用來這個通配符後, 目錄下有多少子目錄都會被匹配到

*表示匹配除了目錄分隔符(/)外的任何長度的字元串

?表示匹配一個除文件分隔符(/)外的任一字元

顯然./src/**/*即表示匹配src文件夾下的任何子文件夾的任何文件; 而./demo/**/*.tsx?即表示匹配demo目錄下任何子目錄下的任意以.ts或.tsx結尾的文件

include其實就是一個白名單, 在這個白名單里被匹配到的文件才會被tsc處理編譯

相對於include是作為白名單的配置, exclude選項就是一個黑名單了, 它的值和include一樣是一個路徑名字元串數組, 最常見的用處就是用來排除掉node_modules目錄下的文件

{
    "compilerOptions": {
        ...
    },
    include: ["./src/**/*", "./demo/**/*.tsx?"],
    exclude: ["node_modules/**/*"]
}

當然也可以用exclude排除掉include裡面包含到的文件

有些情況即使exclude了某些文件後, 編譯後的程式碼中可能仍然包含被exclude了的內容, 比如通過import導入了被exclude了的node_modules文件夾, 此時tsc仍然會去處理被exclude了的文件, 這一點應該不難理解

files 配置的作用類似include, 也是一個白名單路徑數組, 不同在於它不能使用通配符, 而必須使用精確的文件路徑(可以是相對路徑), 比如如果項目只有一個入口文件, 那麼就可以使用在只用files配置這個文件的路徑, 然後其他的文件都通過index.ts來import

tsconfig.json
{
    "compilerOptions": {
        ...
    },
    // include: ["./src/**/*", "./demo/**/*.tsx?"],
    // exclude: ["node_modules/**/*"]
    files: ['./src/index.ts']
}

extends配置

extends 用於在一個tsconfig.json文件中擴展其他tsconfig.json文件, 比如angular項目中有三個tsconfig配置文件: tsconfig.json, tsconfig.spec.json, tsconfig.app.json

tsconfig.json
/* To learn more about this file see: //angular.io/config/tsconfig. */
{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "es2015",
    "module": "es2020",
    "lib": [
      "es2018",
      "dom"
    ]
  },
  ...
}
tsconfig.app.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/app",
    "types": []
  },
  "files": [
    "src/main.ts",
    "src/polyfills.ts"
  ],
  "include": [
    "src/**/*.d.ts"
  ]
}
tsconfig.spec.json
/* To learn more about this file see: //angular.io/config/tsconfig. */
{
  "extends": "./tsconfig.json",
  ...
  "files": [
    "src/test.ts",
    "src/polyfills.ts"
  ],
  "include": [
    "src/**/*.spec.ts",
    "src/**/*.d.ts"
  ]
}

從命名和文件內容上即可看出之所以這麼做是為了針對測試文件.spec.ts和普通.ts文件在使用不同的配置時又能共享他們相同部分的配置, 達到避免重複的目的

compilerOptions下的配置

compilerOptions.allowUnreachableCode

表示是否允許程式碼中出現永遠無法被執行到的程式碼, 可選值是undefined, false, true

{
    "compilerOptions": {
        "allowUnreachableCode": false
        ...
    },
    ...
}

什麼是"永遠無法被執行到的程式碼"?

const foo = () => {
    return 0;

    console.log('aaa'); // 這一行程式碼就是永遠被執行到的程式碼
}

配置為undefined時, 遇到這種情況會給出warning, 配置false則直接編譯時拋出錯誤無法成功編譯, 配置為true既沒有警告也沒有錯誤

compilerOptions.allowUnusedLabels

這個選項是針對標籤(label)語法的, 這個語法很罕見, 我也是看到了這個配置才知道有這個原來js還有Label語法, label語法有點像其他語言里的goto, 真是場景中幾乎不用

compilerOptions.allowUnusedLabels表示是否允許未使用到的標籤

可選項:

  • undefined: 這是默認值, 碰到未使用的標籤給出warning警告
  • false: 碰到未使用的標籤拋出錯誤, 編譯失敗
  • true: 碰到未使用的標籤編譯通過, 且不給出異常
function bar() {
    console.log('loafing...');

    loop: for (let i = 0; i < 3; i++) {
        for (let j = 0; j < 3; j++) {
            if (i === 2) {
                // break loop;
            }
            console.log(i, j, i + j);
        }
    }
}

image

compilerOptions.alwaysStrict

默認值是true, 開啟這個選項保證輸出的js程式碼處於ECMAScript標準的嚴格模式下, 也就是js文件里的use strict

compilerOptions.exactOptionalProperties

這是typescript4.4中才加入的一個選項, 默認處於不開啟狀態; 開啟此選項, typescript會對可空屬性執行更嚴格的類型檢查, 可空屬性只有在初始化時可以留空為undefined, 但是不能被手動設置為undefined

例如有一個IFoo介面

interface IFoo {
  foo?: string;
}

在compilerOptions.exactOptionalProperties = false情況下

const obj: IFoo = {};
obj.foo = '1111';
console.log(obj.foo);
obj.foo = undefined;
console.log(obj.foo);

這段程式碼可以正常編譯通過

但如果開啟compilerOptions.exactOptionalProperties選項後情況就不同了

const obj: IFoo = {};
obj.foo = '1111';
console.log(obj.foo);

// 編譯器會報: Type 'undefined' is not assignable to type 'string' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the type of the target.
obj.foo = undefined;
console.log(obj.foo);

// 這一行會報: Type '{ foo: undefined; }' is not assignable to type 'IFoo' with 'exactOptionalPropertyTypes: true'. 
const obj2: IFoo = {
  foo: undefined
}

compilerOptions.downlevelIteration

先解釋下什麼是Downleveling? Downleveling是Typescript中的一個術語, 它表示將typescript程式碼轉譯為相對更低版本的javascript

這個標誌位模式是不開啟的.

開啟這個標誌位typescript會生成一個幫助方法來對es6中的for of和數組展開([...arr])語法進行轉譯, 以兼容es3/es5

下面的示例用for of循環並輸出一個包含符號表情的字元串:

const text = `(😜)`;
for (const c of text) {
  console.log(c);
}

如果配置typescript的compilerOptions.target選項為es6及以上, 那麼不管有沒有開啟compilerOptions.downlevelIteration, 輸出都是:

(
😜
)

現在把compilerOptions.target設置為es5並關閉compilerOptions.downlevelIteration, 輸出:

image

控制台中一共有四行, 中間兩行是兩個亂碼, 這跟我們的預期的就不一樣了, 我們預期應該只有三行輸出, 實際結果確是符號表情被分成了兩個亂碼字元輸出了;

這是因為沒開啟compilerOptions.downlevelIteration, typescript處理for of時會直接轉譯成經典的索引迭代(for (var _i = 0, text_1 = text; _i < text_1.length; _i++)), 而符號表情實際上佔了2個位元組的存儲, 所以自然就會將符號表情分成兩個亂碼字元輸出了

現在再開啟compilerOptions.downlevelIteration, 此時typescript處理for...of時會通過輔助方法調用數組對象的Symbol.iterator屬性做一些額外的檢查和處理, 就能正常輸出了

image

此時生成的js程式碼:

"use strict";
var __values = (this && this.__values) || function(o) {
    var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
    if (m) return m.call(o);
    if (o && typeof o.length === "number") return {
        next: function () {
            if (o && i >= o.length) o = void 0;
            return { value: o && o[i++], done: !o };
        }
    };
    throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
var e_1, _a;
var text = "(\uD83D\uDE1C)";
try {
    for (var text_1 = __values(text), text_1_1 = text_1.next(); !text_1_1.done; text_1_1 = text_1.next()) {
        var c = text_1_1.value;
        console.log(c);
    }
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
    try {
        if (text_1_1 && !text_1_1.done && (_a = text_1.return)) _a.call(text_1);
    }
    finally { if (e_1) throw e_1.error; }
}

更具體的說明看官網: //www.typescriptlang.org/tsconfig#downlevelIteration

compilerOptions.importHelpers

上面介紹了compilerOptions.downlevelIteration選項, 開啟後會對for...of之類的迭代語法糖進行downleveling; typescript進行downleveling時, 會生成一些輔助方法, 默認情況下, 這些輔助程式碼是會直接插入到文件中對應的位置的, 這會生成的javascript存在重複的輔助方法從而造成程式碼文件體積過大的問題.

開啟compilerOptions.importHelpers後, 不在插入具體的輔助方法的程式碼到對應的位置, 而是通過模組導入來引用typescript的輔助方法

看這段typescript

export const foo = () => {
  const text = `(😜)`

  for (const c of text) {
    console.log(c)
  }
}

沒開啟compilerOptions.importHelpers時, 生成的javascript:

var __values = (this && this.__values) || function(o) {
    var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
    if (m) return m.call(o);
    if (o && typeof o.length === "number") return {
        next: function () {
            if (o && i >= o.length) o = void 0;
            return { value: o && o[i++], done: !o };
        }
    };
    throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
export var foo = function () {
    var e_1, _a;
    var text = "(\uD83D\uDE1C)";
    try {
        for (var text_1 = __values(text), text_1_1 = text_1.next(); !text_1_1.done; text_1_1 = text_1.next()) {
            var c = text_1_1.value;
            console.log(c);
        }
    }
    catch (e_1_1) { e_1 = { error: e_1_1 }; }
    finally {
        try {
            if (text_1_1 && !text_1_1.done && (_a = text_1.return)) _a.call(text_1);
        }
        finally { if (e_1) throw e_1.error; }
    }
};

開啟compilerOptions.importHelpers後, 生成的javascript:

import { __values } from "tslib";
export var foo = function () {
    var e_1, _a;
    var text = "(\uD83D\uDE1C)";
    try {
        for (var text_1 = __values(text), text_1_1 = text_1.next(); !text_1_1.done; text_1_1 = text_1.next()) {
            var c = text_1_1.value;
            console.log(c);
        }
    }
    catch (e_1_1) { e_1 = { error: e_1_1 }; }
    finally {
        try {
            if (text_1_1 && !text_1_1.done && (_a = text_1.return)) _a.call(text_1);
        }
        finally { if (e_1) throw e_1.error; }
    }
};

compilerOptions.strict

這個strict相關標誌位的一個總開關, 設置為true會啟用全部compilerOptions.strict開頭的選項和其他相關的選項, 如compilerOptions.strictNullChecks, compilerOptions.strictPropertyInitialization, compilerOptions.noImplicitAny ...

開啟此選項後, 依然可以單獨關閉某個具體的以compilerOptions.strict開頭的選項

compilerOptions.strictBindCallApply

開啟此選項後, 調用函數對象的bind call和apply方法時typescript會執行參數類型檢查確保參數類型兼容

const foo = (a: string, b: number) => {
  console.log(a, b);
}

// 開啟 strictBindCallApply 後,會報錯
// Argument of type 'number' is not assignable to parameter of type 'string'.
foo.call(undefined, 1, 2)

compilerOptions.strictFunctionTypes

開啟此選項會啟用嚴格的函數類型檢查, 直接看示例:

const foo = (a: string) => {
  console.log(a);
}

interface Bar {
  (a: string | string[]): void;
}

// 開啟 compilerOptions.strictFunctionTypes, 報錯
// Type '(a: string) => void' is not assignable to type 'Bar'.
const bar:Bar = foo;

修改成:

const foo = (a: string) => {
  console.log(a);
}

interface Bar {
  (a: string): void;
}

const bar:Bar = foo;

才能通過編譯

compilerOptions.strictNullChecks

開啟此選項讓typescript執行嚴格的null檢查

const foo: string|null|undefined = undefined;

// 不開啟 compilerOptions.strictNullChecks ,不會有編譯時錯誤,但是運行時會出現異常(Cannot read properties of undefined )
// 開啟 compilerOptions.strictNullChecks,會出現編譯時錯誤(Object is possibly 'undefined')
console.log(foo.length)

compilerOptions.strictPropertyInitialization

開啟此選項讓typescript嚴格的對象屬性初始化檢查

開啟後這段程式碼會出現編譯時錯誤:

class Foo {
  // Property 'foo' has no initializer and is not definitely assigned in the constructor.
  foo: string;
}

改成

class Foo {
  foo = 'foo';
}

或者

class Foo {
  foo: string;

  constructor() {
    this.foo = '';    
  }
}

或者在屬性後加感嘆號進行非空斷言(non-null assertion)

class Foo {
  foo!: string;
}

方能編譯通過

compilerOptions.noImplicitAny

這個配置還是比較好理解的, 就是開啟此選項後, 如果你聲明一個沒有標註類型的變數, 編譯器會會給你一個編譯時錯誤(Parameter 'arg' implicitly has an 'any' type.)

image

給foo函數參數arg標註一個string類型可以消除這個錯誤

const foo = (arg: string) => { console.log(arg) };

foo('hello');

另外要注意如果開啟了compilerOptions.strict選項, 那麼這個選項默認就會處於開啟狀態, 除非手動將這個選項配置為false

compilerOptions.noImplicitOverride

typescript 4.3中才引入的配置

這個選項從名字上也是比較好理解的; 開啟此選項保證子類重寫基類的方法時, 必須在方法前加上override關鍵詞

class BillBuilder {
    build() {}
}

class MonthBillBuilder extends BillBuilder {
    // 開啟 compilerOptions.noImplicitOverride 後,重寫 build 方法必須顯示加上 override 關鍵詞,否則編譯器會報錯:
    // This member must have an 'override' modifier because it overrides a member in the base class 'BillBuilder'.
    build() {
        console.log('Monthly bill')
    }
}

正確的寫法:

class BillBuilder {
    build() {}
}

class MonthBillBuilder extends BillBuilder {
    override build() {
        console.log('Monthly bill')
    }
}

compilerOptions.noImplicitReturns

開啟這個選項保證編譯時所有條件分支都返回一致的類型, 比如說一個if分支下返回了一個string類型, 但是其他分支沒有進行return, 那麼tsc會給出一個編譯時錯誤(Not all code paths return a value.(7030) )

image

正確的寫法1:

const hello = (log = false) => {
    if (log) {
        const text = 'hello';
        console.log(text);
        return text
    }

    return '';
}

正確的寫法2:

const hello = (log = false): string | void => {
    if (log) {
        const text = 'hello';
        console.log(text);
        return text
    }
}

compilerOptions.noImplicitThis

開啟這個選項後, typescript將禁止調用any類型的this

錯誤示例:

function Color() {
    // 開啟了ompilerOptions.noImplicitThis的情況下,下面的三行程式碼會出現編譯時錯誤
    // 'this' implicitly has type 'any' because it does not have a type annotation.
    this.r = 255;
    this.g = 255;
    this.b = 255;
}

// 如果開啟了 compilerOptions.noImplicitAny , 那麼這一行也是會報編譯時錯誤的:
// 'new' expression, whose target lacks a construct signature, implicitly has an 'any' type.
const c = new Color();

console.log(c.r, c.r, c.b);

正確的寫法:

class Color {
    r = 255;
    g = 255;
    b = 255;
}

const c = new Color();

console.log(c.r, c.r, c.b);

compilerOptions.noPropertyAccessFromIndexSignature

這個配置選項typescript4.2中才引入

從名稱入手來理解這個配置, no property access from index signature, 就是說開啟後禁止通過訪問常規屬性的方法來訪問index signature聲明的屬性

常規屬性通過在對象後加一個.即可訪問如obj.title

什麼是index signature? 直譯過來是索引簽名, 索引簽名語法一般用來聲明介面或類中的未知屬性的, index signature的示例:

// 標註為IFoo類型的對象可以添加任意的字元串鍵值對
interface IFoo {
  [key: string]: string;
}

const foo: IFoo = {
  bar: '1'
}

console.log(foo['bar']);
// 輸出: 1

開啟compilerOptions.noPropertyAccessFromIndexSignature後的一個錯誤示例:

class Color {
    r = 255;
    g = 255;
    b = 255;

    [key: string]: string | number;
}

const c = new Color();

console.log(c.r, c.g, c.b);

// 開啟 compilerOptions.noPropertyAccessFromIndexSignature 的情況下,會有編譯時錯誤:
// Property 'foo' comes from an index signature, so it must be accessed with ['foo'].
console.log(c.foo);

正確的方式應該是通過c['foo']訪問Color對象c上的foo屬性

這個選項的動機是什麼?

不開啟compilerOptions.noPropertyAccessFromIndexSignature直接通過傳統的.符號來訪問索引簽名屬性, 一不小心非常容易造成運行時出現經典的read property of undefined異常, 比如上面的示例如果直接調用c.foo.toString(), 即使開啟了compilerOptions.strictNullChecks編譯仍能通過, 但是運行就會發生異常; 當然也可以顯示標註索引簽名屬性為可空類型配置compilerOptions.strictNullChecks來達到相同的目的

class Color {
    r = 255;
    g = 255;
    b = 255;

    [key: string]: string | number | undefined;
}

const c = new Color();

console.log(c.r, c.g, c.b);

// 關閉 compilerOptions.noPropertyAccessFromIndexSignature,開啟 compilerOptions.strictNullChecks
// 仍然可以獲得編譯時的對象屬性可能為空的錯誤:
// Object is possibly 'undefined'.
console.log(c.foo.toString());

compilerOptions.noUncheckedIndexedAccess

這個配置選項typescript4.1中才引入

開啟這個選項, typescript自動給索引簽名語法聲明的屬性補上一個undefined類型; 上面介紹compilerOptions.noPropertyAccessFromIndexSignature時提到可以自己手動給索引簽名語法聲明的屬性加上undefined類型標註達到和開啟compilerOptions.noPropertyAccessFromIndexSignature相同的目的

image

看上面的圖片中雖然沒有顯示給索引簽名屬性標註undefined, 但是滑鼠懸浮到c.foo上時typescript自動給它加上了undefined

compilerOptions.noUnusedLocals

又是一個很好理解的配置選項, 開啟這個選項, 當typescript發現未使用的局部變數時, 會給出一個編譯時錯誤('<propertyName>' is declared but its value is never read.(6133))

const sayHello = () => {
    // 開啟 compilerOptions.noUnusedLocals 後,typescript編譯時會報錯
    // 'text' is declared but its value is never read.
    const text = 'hello';
    console.log('hello');
}

✅正確的做法:

const sayHello = () => {
    const text = 'hello';
    console.log(text);
}

compilerOptions.noUnusedParameters

和上面的compilerOptions.noUnusedLocals, 不同之處在於局部變數變成了函數參數

❌錯誤示例:

// 開啟 compilerOptions.noUnusedParameters 後,typescript編譯時會報錯
// 'text' is declared but its value is never read.
const sayHello = (text: string) => {
    console.log('hello');
}

✅正確的寫法:

// 開啟 compilerOptions.noUnusedParameters 後,typescript編譯時會報錯
// 'text' is declared but its value is never read.
const sayHello = (text: string) => {
    console.log(text);
}

compilerOptions.useUnknownInCatchVariables

這個配置選項typescript4.4中才引入

開啟這個選項typescript會將catch語法塊中的err變數當做unknown來處理, 不開啟此選項時, err變數是被當做any類型來處理的, 這很容易造成經典的read property of undefined運行時異常

一個簡單的示例, 不開啟compilerOptions.useUnknownInCatchVariables選項運行時才能發布異常

image

開啟了compilerOptions.useUnknownInCatchVariables選項, 編譯時立即發現問題

image

小結

本文主要介紹了typescript中類型檢查相關的配置, typescript還有其他不少配置的, 官網都有詳細的文檔

我對typescript一些看法

我是18年大二階段開始接觸前端相關的程式語言的, 那個時候typescript還沒有這麼流行, 接觸使用學習時還是以js為主的; 此前主主要使用的語言的是C/C++ C#這些強類型語言, 對js存在強烈的抵觸甚至厭惡, 然後接觸到angular時發現它的整個生態都是構建在typescript之上的, 終於遇到了救星; 雖然那個時候剛剛接觸, angular用起來也是一知半解的, 常常被typescript中如何使用js庫之類的小白問題支配, 但是相比要我寫沒有類型註解的javascript, 那時的我依然選擇慢慢摸索解決typescript中如何使用js庫這樣的小白問題, 解決這些問題過程中, 我對js也有了更深入的理解, 慢慢的我甚至可以脫離typescript學會寫javascript了😂typescript的定位是javascript的超集, 與我而言, typescript確實我在javascript上的老師, 沒有typescript, 那個時候的我可能已經放棄學習javascript了, 就像叫不醒一個裝睡的人一樣, 我們永遠也學不會一門自己不想學設置討厭的的程式語言; 也不是說沒有typescript就永遠都不會對javascript產生興趣, 而是產生興趣的時間會延後到不知何時;

typescript是一個良師益友, 不敢想像沒有typescript的世界將會是怎樣😇或許也並不會怎樣, 只不過少了一個愛寫前端的靚仔而已🌚