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.tsimport

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 callapply方法时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的世界将会是怎样😇或许也并不会怎样, 只不过少了一个爱写前端的靓仔而已🌚