3

tsconfig常用配置全解 - Laggage

 1 year ago
source link: https://www.cnblogs.com/laggage/p/explain-some-options-of-tsconfi.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

更具体的说明看官网: 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 = "(\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的世界将会是怎样😇或许也并不会怎样, 只不过少了一个爱写前端的靓仔而已🌚


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK