tsconfig常用配置全解

基于typescript的项目的根目录下都会有一个文件(tsconfig.json), 这个文件主要用来控制typescript编译器(tsc, typescript compiler)的一些行为, 比如指定哪些文件需要让tsc来编译, 哪些文件不想让tsc进行编译之类的.

angular项目的tsconfig.json文件

tsconfig.json
/* To learn more about this file see: https://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: https://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: https://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);         }     } } 

tsconfig常用配置全解

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, 输出:

tsconfig常用配置全解

控制台中一共有四行, 中间两行是两个乱码, 这跟我们的预期的就不一样了, 我们预期应该只有三行输出, 实际结果确是符号表情被分成了两个乱码字符输出了;

这是因为没开启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属性做一些额外的检查和处理, 就能正常输出了

tsconfig常用配置全解

此时生成的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 = "(uD83DuDE1C)"; 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; } } 

更具体的说明看官网: https://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 = "(uD83DuDE1C)";     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 = "(uD83DuDE1C)";     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.)

tsconfig常用配置全解

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) )

tsconfig常用配置全解

正确的写法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相同的目的

tsconfig常用配置全解

看上面的图片中虽然没有显示给索引签名属性标注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选项运行时才能发布异常

tsconfig常用配置全解

开启了compilerOptions.useUnknownInCatchVariables选项, 编译时立即发现问题

tsconfig常用配置全解

小结

本文主要介绍了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的世界将会是怎样😇或许也并不会怎样, 只不过少了一个爱写前端的靓仔而已🌚

发表评论

相关文章