- include, exclude, files配置项
- extends配置
- compilerOptions下的配置
- compilerOptions.allowUnreachableCode
- compilerOptions.allowUnusedLabels
- compilerOptions.alwaysStrict
- compilerOptions.exactOptionalProperties
- compilerOptions.downlevelIteration
- compilerOptions.importHelpers
- compilerOptions.strict
- compilerOptions.strictBindCallApply
- compilerOptions.strictFunctionTypes
- compilerOptions.strictNullChecks
- compilerOptions.strictPropertyInitialization
- compilerOptions.noImplicitAny
- compilerOptions.noImplicitOverride
- compilerOptions.noImplicitReturns
- compilerOptions.noImplicitThis
- compilerOptions.noPropertyAccessFromIndexSignature
- compilerOptions.noUncheckedIndexedAccess
- compilerOptions.noUnusedLocals
- compilerOptions.noUnusedParameters
- compilerOptions.useUnknownInCatchVariables
- 小结
基于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.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: 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); } } }
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
, 输出:
控制台中一共有四行, 中间两行是两个乱码, 这跟我们的预期的就不一样了, 我们预期应该只有三行输出, 实际结果确是符号表情被分成了两个乱码字符输出了;
这是因为没开启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
属性做一些额外的检查和处理, 就能正常输出了
此时生成的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
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.
)
给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)
)
正确的写法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
相同的目的
看上面的图片中虽然没有显示给索引签名属性标注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
选项运行时才能发布异常
开启了compilerOptions.useUnknownInCatchVariables
选项, 编译时立即发现问题
小结
本文主要介绍了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的世界将会是怎样😇或许也并不会怎样, 只不过少了一个爱写前端的靓仔而已🌚