我们之前学习的都是Typescript的一些上层用法, 期间也介绍过配置相关的内容, 诸如: noImplicitAny、strictNullChecks等, 但都是简单带过, 现在, 我们就开始系统化熟悉下tsconfig.json中的常见配置
源码相关
experimentalDecorators
experimentalDecorators, 其实看名字就知道了, 就是启用装饰器, 即允许我们在源码中使用装饰器, 前面两节的内容都是基于开启了experimentalDecorators配置进行的, 该配置项默认是关闭的, 所以必须手动开启才能使用!
emitDecoratorMetadata
emitDecoratorMetadata是一个编译选项, 作用在于开启装饰器元数据; 这么说可能不清晰, 还记得上一节中我们利用Reflect.getMetadata('design:type', prototype, propName)来获取属性类型吗? 在那个案例中, 我们利用了内置的元数据design:type获取了成员的类型
function Inject(key?: string): PropertyDecorator {
return function (prototype: any, propName: string | symbol) {
Container.regsiterProperty(
`${prototype.constructor.name}:${String(propName)}`,
key ?? Reflect.getMetadata('design:type', prototype, propName)
);
};
}
@Provide()
class Feul {
NO: string = '95#';
}
@Provide()
class Car {
@Inject()
feul!: Feul;
constructor() {}
run() {
console.log(this.feul.NO);
}
}
在这个案例中, 如果我们没有给Inject装饰器传递任何参数, 那么, 就会使用design:type, 来获取成员类型, 以此作为从第一个映射表中获取类型值的键, 如果我们关闭emitDecoratorMetadata选项, 即设为false, 那么这个案例在没有给Inject传參的情况下, 会在运行时报错! 因为design:type元数据根本获取不到任何信息!
该选项同样默认关闭, 如需使用必须手动开启;
jsx
jsx配置主要控制的就是jsx语法的编译结果, 其值有: preserve、react-native、react、react-jsx、react-jsxdev;
preserve: 顾名思义, 就是保持jsx的语法不变, 生成.jsx文件
// 源码 .tsx
import React from 'react';
const component = () => <div>123</div>;
// 产物 .jsx
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const react_1 = __importDefault(require("react"));
const component = () => <div>123</div>;
react-native: react-native和preserve产物是一样的, 只是生成的文件是.js文件
// .jsx
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const react_1 = __importDefault(require("react"));
const component = () => <div>123</div>;
react: react则是将jsx代码转为react.createElement的形式
// .js
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const react_1 = __importDefault(require("react"));
const component = () => react_1.default.createElement("div", null, "123");
react-jsx: 启用react/jsx-runtime进行代码编译; react/jsx-runtime是react17新出的代码转换工具包, 在这之前, react中的jsx通常都转为react.createElement的形式, 而jsx-runtime则提供了更多的优化, 例如: 减小编译后的体积、减少运行时开销、更好的类型检查等等;
// .js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const jsx_runtime_1 = require("react/jsx-runtime");
const component = () => (0, jsx_runtime_1.jsx)("div", { children: "123" });
react-jsxdev: 启用react/jsx-dev-runtime进行代码编译, 其实就是jsx-runtime的开发环境版, 提供了一些调试工具
// .js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const jsx_dev_runtime_1 = require("react/jsx-dev-runtime");
const _jsxFileName = "/Users/wangy/IT/eslint-demo/src/component.tsx";
const component = () => (0, jsx_dev_runtime_1.jsxDEV)("div", { children: "123" }, void 0, false, { fileName: _jsxFileName, lineNumber: 2, columnNumber: 24 }, this);
console.log('🚀 ~ file: component.tsx:3 ~ component:', component());
其他的jsx相关的配置
jsxFactory: 其实就是jsx: 'react'时的附属配置项, 决定调用的函数, 默认为React.createElement; 可以将其改为'h', 即启用preact.h方法
jsxFragmentFactory: jsxFactory影响的是普通jsx组件, 则jsxFragmentFactory只针对Fragment组件, 即空白标签组件<></>; 值为处理Fragment组件的方法
jsxImportSource: jsx: 'react-jsx' 或 'react-jsxdev'的附属选项, 其值就是jsx-runtime/jsx-dev-runtime的提供方; 即 决定了jsx-runtime/jsx-dev-runtime从哪里引入;
target
target很好理解, 就是编译后的代码的es版本, 比如es5、es6、es2022这ECMAScript版本号, 如果我们指定target为es5, 那么, 诸如箭头函数、async await 这类较高版本的语法就会被转换;
{
"compilerOptions": {
"target": "ES5",
}
}
// 源码 .ts
const fn = () => {
console.log(111)
}
fn()
// --target es5
"use strict";
var fn = function () {
console.log(111);
};
fn();
相应的, 如果target版本足够高, 已经高于所用的语法了, 那么, 箭头函数就会被保留
"use strict";
const fn = () => {
console.log(111);
};
fn();
lib
lib 的值是一个数组; 表示开发阶段, 需要引入的库, 我们知道Typescript有许多内置的库, 比如我们之前的很多工具类型, 其实也是源于Typescript的内置库, 其位置就在typescript文件根目录中
通常情况下, lib会被自动导入; 比如, 当我们的target设置为ES2021的时候, lib其实会自动导入ES2021这个库的所有内容; 如果你所需要用到的语法需要用到指定的编译库, 那么就需要特别指明
// 源码 .ts
let str = '1'
str.replaceAll('1', '2')
此时由于replaceAll是es2021的方法, 而我们的target为es6, 所以此时会提示错误;
此时可以修改lib配置项
{
"compilerOptions": {
// ...
"lib": ["ES2021.String"],
// ...
}
}
这样, 就不会报错了;
但是要注意, 如果你此时设置了lib这个配置项, 那么你所设置的这一项将会代替掉默认导入内容! 接上面的案例, 我们现在将target设置为ES2021, 但是, 将lib设置为ES2021.Promise; 注意, 哪怕他们都是同一个版本下的库, 可会造成覆盖:
{
"compilerOptions": {
// ...
"target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"lib": ["ES2021.Promise"]
// ...
}
}
此时, 报错又出现了:
当然, 这种情况仅仅针对那些需要引入特殊库的语法, 对于一些相对没那么新的语法, 比如: Promise、startsWith等, 是不怕这种覆盖的
noLib
noLib值为一个boolean类型, 这个选项一旦设为true, 除了一些外部依赖, 比如@types/node所声明的内容, typescript这个包中的一切类型声明的库, 都将被禁止导入! 也就是说, 一旦开启, 你要重新为所有的对象、数组、字符串声明类型!否则你将失去绝大多数类型提示;
构建解析
files、include、exclude
这三个配置之所以放在一起讲解, 是因为他们都是规定本次编译的范围! 只不过具体功能有所不同;
files就是告诉编译器, 本次需要编译那几个文件; 接受一个数组作为参数值
{
"files": ["./test.ts"],
}
注意: files、include、exclude三个配置均不在compilerOptions之内!
include, 则是告诉编译器, 哪些文件需要被编译, 注意, files毕竟只能指定文件, 而当项目日益变大之后, 配置也会越来越多, 指定特定的文件, 显然是不合适的, 所以还必须要有一种'杀伤范围'更大的‘武器’, 这就是include, 它接受一个数组, 使用glob pattern格式
{
"include": ["./src/**/*", "./build/*", "util/*.ts"],
}
配置的含义分别为:
- 匹配src下的所有文件, 无视层级;
- 匹配build下的合法文件, 仅此一层, ./build/types/这种级别的文件不在考虑范围;
- 匹配util下的所有.ts文件, 和上面一样, 仅此一层;
上面说的合法文件, 指的是.ts、.tsx、.d.ts、.js、.jsx, 其中js必须开启allowJs配置项
但是有的时候, 我们include匹配过头了, 可能把一些不想要的文件也匹配进来了, 那怎么办? 这些文件也是合法的, 此时, 就需要用到exclude, 从include中去删除个别文件了! 注意, 是从include中; 如果一个文件本就不属于include匹配范围内, 那exclude也是无效的;
{
"include": ["./src/**/*", "./build/*", "util/*.ts"],
"exclude": ["./src/**/*.ignore.ts"]
}
baseUrl
baseUrl表示的是, 解析绝对路径时的根目录,例如: import util from 'util/array', 这个util表示在根目录下的util文件夹, 那么哪里是根目录? 此时, 就需要由baseUrl来确定了
{
"compilerOptions": {
"baseUrl": "./",
}
}
这个配置表示, 根目录就在tsconfig.json所在的目录下, 此时, util文件夹必须和tsconfig.json同级; 而如果我们将baseUrl改为"./src/", 则根目录就会变成src下的目录, util也就必须在src下; 否则就会提示错误; 不过要注意: baseUrl只会影响绝对路径, 对于大多数相对路径的是没有影响的, 除了paths这个配置项, 这个后续会介绍; 另外, baseUrl仅仅只是提供一个提示, 它不会对编译后的内容产生任何的改变, 例如使用如下配置:
{
"compilerOptions": {
"baseUrl": "./src",
}
}
代码编译后不会发生改变:
// 源码
import util from 'util/array'
// 编译后
import util from 'util/array'
我们会发现, 编译后的代码并没有被转为./src/util/array, 因此, baseUrl仅作为一个提示存在, 它不参与编译;
rootDir
rootDir, 从这个名字就能看出,它也代表根目录, 那这就奇怪了, 有了baseUrl, 还要它作甚? 其他它这个根路径和baseUrl的起的作用不同; rootDir可以说表示的是能够参与编译的根目录; 它的默认值为ts文件的最大公共路径
projectRoot
├── src
│ ├── index.ts
│ ├── utils
│ │ ├── index.ts
├── declare.d.ts
├── tsconfig.json
以上目录结构中的ts的最大公共路径就是src, rootDir默认值也是./src, 因为src外面再无.ts文件了, .d.ts声明文件不算; 而如果我们在src外再加一个.ts文件
projectRoot
├── src
│ ├── index.ts
│ ├── utils
│ │ ├── index.ts
├── declare.d.ts
├── newFile.ts // 新增
├── tsconfig.json
则此时的rootDir的默认值就是这个工程的根目录, 即rootDir: "./"; 那么, 知道这个根路径有啥用呢? 注意, ts编译器会将rootDir表示的根目录中的所有可编译(include或范围内)内容, 都编译并放到目标文件夹内!以上两种文件编译后的结构分别为:
"rootDir": "./"
dist
├── src
│ ├── index.js
│ ├── utils
│ │ ├── index.js
├── newFile.js
"rootDir": "./src"
dist
├── index.js
├── utils
│ ├── index.js
注意了, 上面都是rootDir范围大于include的情况, 那如果反过来呢? 万一include比rootDir还大呢? 当然, 现实中不可能, 因为如果你这么干了, 你只会收到一条报错! 如果你无视报错强行编译, 那么, 内容还是会以rootDir的为准! 即 只编译更小范围的那部分! 当然, 我们最好别这样, 还是要让include的内容, 在rootDir的范围内;
rootDirs
rootDirs其实顾名思义就是多个根目录, 虽然名字和rootDir很像, 但是它和rootDir并不相同, 它指定了多个目录为根路径, 但是它不会影响编译结果, 它的意义在于虚拟目录的合并;假设我们的目录结构如下:
projectRoot
├── root-1
│ ├── index1.ts
├── root-2
│ ├── index2.ts
├── root-3
│ ├── index3.ts
├── tsconfig.json
如果此时我们在root-1/index1.ts中要引入index2.ts,势必要这样写
import xx from '../root-2/index2'
而如果我们增加了如下配置:
{
"compilerOptions": {
"rootDirs": ["root-1","root-2","root-3"],
}
}
我们就可以直接引入, 就好像他们三个index文件在同一个目录下一样
import xx from './index2'
但是要注意, 编译结果不会因这个配置而改变! 这里只是虚拟的一个路径
types 和 typeRoot
默认情况下, ts会自动加载node_modules/@types中的所有文件, 而types, 就是指定加载某几项
{
"compilerOptions": {
"types": ["node","react"], // 仅加载@types/node 以及@types/react
}
}
前面说了, 系统会自动从node_modules/@types下搜索对应的文件, 但是, 如果我们想自定义类型声明文件加载路径, 可以修改typeRoot配置项
{
"compilerOptions": {
"typeRoot": ["node_modules/@types","./my-types"], // 仅加载@types/node 以及@types/react
"skipLibCheck": true
}
}
加载多个类型声明文件可能会导致冲突, 通过配置skipLibCheck可以避免这个问题的出现
paths
paths其实就是别名, 类似于webpack中的alias
{
"compilerOptions": {
"paths": {
"@src/*": ["./src/*"]
}
}
}
这里的含义其实就是定义了./src的路径别名为@src, 当然就像前面说的, 它是受baseUrl影响的, 即使它是相对路径!
resolveJsonModule
此配置项为是否开启解析json文件功能, 开启后, 就能够解析json模块, 并对json内容提供提示以及类型校验
{
"compilerOptions": {
"resolveJsonModule": true
}
}
构建产出
输出相关
outDir & outFile
outDir顾名思义, 其实就是存放构建产物的文件夹, 一半会按照源码的目录结构, 存放在outDir指定的文件夹内; 而outFile则是将所有内容都编译到一个文件中去! 但是要注意这个配置项要求颇为严格, module, 即产物的标准, 必须是amd或system形式!
preserveConstEnums
preserveConstEnums, 看名字就知道, 就是保留常量枚举, 我们的常量枚举默认编译后是不会保留的, 而开启本配置后, 编译的结果仍会保留它;
noEmit & noEmitOnError
noEmit表示, 只构建, 但是, 不把产出物写入文件系统, 适合的场景就是, 不需要产出物, 只是想检查下类型和语法; noEmitOnError则是指, 出错的时候, 才阻止写入系统;
module
module表示构建产物的模块标准, 比如: CommonJs(默认)、amd、umd、esxx、node16、nodenext等等; 要注意这个选项和其他配置项的关联关系, 例如前面的, 如果我们设置了outFile配置项, 则module必须, 也只能是amd和system
importHelpers 与 noEmitHelpers
importHelppers有点类似于babel中的polyfill概念, 即垫片, 或者说辅助函数都行; 就是我们对语法进行降级的时候(target设置成低版本), 有些语法需要辅助函数转换, 此时, 如果importHelpers设置成了true, 那么编译的结果就会自动引入tslib, 这是一个非常重要的typescript库, 可以导出辅助函数而不是直接将辅助的逻辑全部写在编译的结果中, 可以减少编译产物体积, 但是有几点需要注意:
- 你的源码文件必须是一个模块, 即至少有export等导出的操作;
- 你必须安装了tslib;
- 你的module不能为none;
- 不可设置noLib为true;
// 源码:
let arr:any[] = [11,2,3]
export let newArr:any[] = ['new', ...arr]
// 编译后
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.newArr = void 0;
var tslib_1 = require("tslib");
var arr = [11, 2, 3];
exports.newArr = tslib_1.__spreadArray(['new'], arr, true);
noEmitHelpers 则是从tslib中导入辅助函数, 要不自动实现辅助函数, 而是必须由用户去实现这些辅助函数,怎么理解呢? 如果你正常啥也不设置, 只是规定target为低版本语法, 那么编译后的代码其实也会生成辅助函数
// 源码:
let arr:any[] = [11,2,3]
export let newArr:any[] = ['new', ...arr]
// 编译产物:
"use strict";
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.newArr = void 0;
var arr = [11, 2, 3];
exports.newArr = __spreadArray(['new'], arr, true);
如果你设置了importHelppers为true, 那结果就是刚才说的那样, 引入了tslib, 不会写那么一大堆东西, 减少了体积; 而如果你设置了noEmitHelpers为true, 那编译结果就会变成这样:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.newArr = void 0;
var arr = [11, 2, 3];
exports.newArr = __spreadArray(['new'], arr, true);
我们可以看到, __spreadArray辅助方法压根没实现, 只能由用户自己在源码中重新实现!
downlevelIteration
在target低于es6的时候, 如果我们源码中使用了for...of, 那么, 在默认的编译中, for...of将被编译为普通for循环
// 源码:
let arr:any[] = [11,2,3]
for (let item of arr) {
console.log(item)
}
// 编译产物:
"use strict";
var arr = [11, 2, 3];
for (var _i = 0, arr_1 = arr; _i < arr_1.length; _i++) {
var item = arr_1[_i];
console.log(item);
}
但是有时候, for..of可能会和for循环出现不一致的情况, 例如: emoji字符在for...of中遍历一次; 而在for循环中则会有两次; 所以, 为了保持for...of不被降级, 可以设置downlevelIteration为true; 但是要注意了, 这里所谓的不降级, 只是在[Symbol.iterator]接口存在的情况下, 用for循环去模拟for...of! 而不是真的不降级;
声明相关
declaration
declaration很好理解, 就是是否要生成类型声明文件, 即 .d.ts文件, 默认不生成, 开启后, 声明文件会跟编译后的文件走, 比如: outDir: './dist', 则声明文件也会出现在dist内
declarationDir
如果我们想改变声明文件生成的位置, 就可以设置declarationDir, 它和outDir一样, 都是指定一个输出文件路径; 比如, 可以设置声明文件全部输出到types文件夹内, "declarationDir": "./types"
declarationMap
我们都知道, sourcemap是源码和编译后代码的映射表, 我们通过sourcemap可以定位到源码的位置, 同样的, 声明文件也有sourcemap, 可以映射声明文件和源码的位置关系, 最典型的就是, 我们使用一些npm第三方库的时候, 点击类型, 往往会跳转到声明文件, 而如果作者设置了declarationMap并且在npm包内提供了ts源码, 则可以直接跳转到对应的ts文件中;
emitDeclarationOnly
emitDeclarationOnly顾名思义, 就是指生成声明文件! 很想noEmit, 都是限制产出物输出的, 一般场景就是, 和其他构建工具配合的时候, tsc只负责生成声明文件, 而其他部分, 则由其他构建工具代为处理;
检查相关
这部分的检查相关的内容, 将在开发阶段对代码进行检查, 并提出警告或者报错, 来规范我们的代码, 其功能也和eslint颇为相似
允许类
允许类的配置会降低代码校验的严格程度, 名字通常都是allowxx, 它可以减少开发阶段代码报错的几率, 但是, 正因为如此, 它也增加了代码出错的几率; 因此, 请谨慎开启!
allowUmdGlobalAccess
我们知道, 代码的模版规范主要有ES Module、CommonJs、AMD、UMD等, 其中UMD最为特别, 它兼容了其他的模版规范, 同时, 会将变量挂载到全局对象之上, 比如: window、global这类; 通常我们可以直接访问window、global上的对象
console.log(a)
// 等价于
console.log(window.a)
不错, 我们可以不需要导入而直接使用它, 如果我们关闭该项, 则以上访问方式会报错
allowUnreachableCode
我们日常开发中, return语句、throw Error语句、process.ext等语句都代表了其所在作用域内的代码执行终结, 这意味着, 该作用域内, 这些语句后面的代码, 都是不会被执行的! 如果我们设置allowUnreachableCode为true, 就会导致编译器忽略这个错误
开启效果:
关闭效果:
allowUnusedLabels
在javascript中, 如果有多个for循环嵌套, 而我们又想在得到结果后就终止整个循环, 此时我们通常会使用label
outerLoop:
for (let i = 0; i < 5; i++) {
innerLoop:
for (let j = 0; j < 3; j++) {
if (i === 2 && j === 1) {
break outerLoop; // 跳出外部循环
}
console.log(`i: ${i}, j: ${j}`);
}
}
以上案例中, 我们给内外两个循环分别设置了一个label: outerLoop 和 innerLoop, 当符合条件的时候, 我们会直接终止外层循环, 而我们并没有使用到内层循环的label , 即innerLoop; 如果我们设置allowUnusedLabels为true, 代码不会有任何提示; 而如果将其改为false, 则会产生警告提示
禁止类
这部分内容会对代码的类型甚至逻辑进行校验, 名字很多都是noxx开启它们有助于保障我们代码的质量, 但同时也会增加一些工作量, 所以, 是否开启, 得视情况而定;
noImplicitAny
这个配置项可以说是很常见的, 即禁止隐式转换any, 我们都知道, any虽然给我们开发提供了便利, 但是也会造成一定的风险; 来看看一下案例
function fn (params) {
params.name = 'jack'
params.age = 18
params.age = '19'
return params
}
以上代码在关闭noImplicitAny时, 不会有任何报错, 但是我们看看, 这里明显是有问题的! 首先, 谁能保证params在运行时一定存在? 或者说不会是null?再者params.age一会是数字, 一会是字符串, 这样随便来, 基本也就丧失了类型支持了; 原因就在于, 我们没有给params设置任何类型, 而此时, 由于我们又允许Typescript将其隐式转为any, 所以, params就自然而然地变成了any, 也就导致了一系列错误; 防止这类错误的办法有2个: 第一就是每次都记住要加类型, 显然不现实, 你记住人家不一定记得住; 第二就是, 乖乖打开noImplicitAny配置项!这样, 如果我们不设置类型, 就会得到一个提示:
当然, 此时如果你将params设为any也没问题; 不过最好被这么做
useUnknownInCatchVariables
当我们使用try...catch的时候, 通常会拿到catch的参数, 然后打印出错误的原因; 如果关闭本选项, catch的参数会变成any类型, 而非unknown! 而如果转为any, 则意味着它可以赋值给任何类型! 比如以下案例, 就不会提示错误, 但显然错得很离谱:
let str:string = ''
function fn () {
try {
// ...
}catch (e) {
str = e
}
}
if (str.startsWith('new-')) {}
noFallthroughCasesInSwitch
如果说以上两种禁止类都是禁止any或者说就是针对类型的, 那么, 接下来我们要介绍的禁止类配置都跟逻辑有关, 更加深入代码的细节中; 我们使用switch...case的时候可能会这样写:
function fn(width, height) {
switch (width * height) {
case 1:
console.log('one');
case 2:
console.log('two');
case 3:
console.log('three');
}
}
fn(1, 1);
// 打印结果:
/**
one
two
three
*/
我们其实可以说我们的case就是出现了Fallthrough问题, 即从上到下贯穿了! 原因就在于没有写break; 所以, 当我们开启noFallthroughCaseInSwitch之后, Typescript就会在开发阶段就提示错误, 从而避免引发更大的问题!
noImplicitOverride
现在只要看到noImplicitxx我们应该就能很快反应过来, 是为禁止某种隐式行为, 就像前面的noImplicitAny一样, 这里的noImplicitOverride其实就是禁止子类隐式覆盖父类的的方法, 我们知道, 子类如果实在必须要覆盖父类的方法, 就必须显性使用override修饰符! 以此来提示, 这里发生了覆盖;
依照里氏替换原则: 子类应该是扩展父类的方法, 而非直接覆盖!
而如果我们的子类覆盖了父类的方法, 而又没有使用override, 就会给代码的维护造成一定的困难
class Animal {
eat() {
console.log('I can eat')
}
}
class Dog extends Animal {
override eat () {
console.log('I will bark!')
}
}
因此, 我们需要开启noImplicitOverride配置项, 来提醒我们要注意在覆盖处添加override修饰符!
noImplicitReturns
设想一个场景: 如果一个函数中有很多的判断条件, 而且我们预先设想是每个判断条件都需要返回值, 而且返回值是允许undefined; 那么, 当我们忘记在某个分支上返回内容的时候, Typescript是否也会无法检测到:
type returnType = string|number|undefined
function fn (num:number):returnType {
if (num > 0) {
return 'this is too much'
} else {
}
}
以上场景中, 我们的fn能够接收string|number|undefined, 可能开发人员只是习惯性拿undefined来兜底, 本质上是要有number类型返回的, 但是由于粗心, 在else分支没有返回内容, 恰好函数又能够接收undefined, 此时, 错误就会被我们忽视; 因此, 我们需要开启noImplicitReturns, 也就是说, 我们必须明确return一个值, 哪怕真的是undefined! 开启之后:
noImplicitThis
我们在javascript开发中, 经常会这样使用this
function fn (name) {
this.name = name
}
咋一看没问题呀, 我这个fn后续会拿去实例化成一个对象, 这么写没问题呀, 但是, 别忘了, this并不是只为实例化服务的, 它指向的是调用它的对象, 那如果调用它的对象里没有name那怎么办?
let obj = {
age: 19,
fn
}
obj.fn()
可能你会觉得也没什么问题, 大不了赋一个值呗! 那如果this后面是一个对象呢? 或者说, 查询的层次很深呢?
function fn () {
this.info.name = 'jack'
}
此时如果info不存在, 就会报出空指针错误; 所以, Typescript提供了noImplicitThis配置项, 当开启的时候, 我们在函数中调用this的时候, 必须为this声明类型! 那怎么声明呢? 在Typescript中, 可以这样为函数的this声明类型:
function fn (this: {name:string}) {
this.name = 'jack'
}
不错, 第一个参数就是this! 如果我们开启了noImplicitThis, 而且在函数中调用了this上的属性/方法, 而且没有声明this的类型, 那么就会提示错误;
这样, 当我们为this声明类型后, 这个方法如果被赋给一个对象的某个属性, 那么这个对象就必须拥有这个方法中, this所拥有的属性/方法, 否则一样报错
noUnusedLocals和noUnusedParameters
开启这两项之后, 在局部作用域内, 变量声明以及参数声明后必须有地方使用, 而不是只声明不用, 否则会报出警告
严格检查
exactOptionalPropertyTypes
我们声明一个对象的时候, 有时候会声明一个可选属性, 但是这个属性使用的时候会出现一定的困惑, 就是这个属性到底是没有呢, 还是为undefined; 显然存在不确定的地方; 开启exactOptionalPropertyTypes之后, 就是明确了这个问题, 即 这个值是不存在或者有非undefined的值, 总之, 要么有非undefined的值, 要么干脆这个属性都没了, 就不可能存在undefined!
alwaysStrict
这个就是开启es5的严格模式
strictBindCallApply
bind、call、apply都可以改变一个方法的this, 同时, 这个方法的参数, 都能由这三个方法代为接收, 开启strictBindCallApply之后, 就是要求这三个方法代收参数的时候, 必须符合原方法的参数类型
strictFunctionTypes
开启逆变, 我们知道, 当A函数的参数是B函数的子类型; 同时, B函数的返回值是A函数的子类型, 则说明B是A的子类型! 这就是逆变(参数父子关系与函数父子关系相反)和协变(返回值父子关系和函数父子关系相同)! 而要开启逆变, 就必须设置strictFunctionTypes为true;
如果我们关闭本选项, 那么只会进行协变检查:
此时, A的参数是B参数的父类型, 或者说, A的参数包含了B, 按照逆变角度来讲, A 是B的子类型, 那么B肯定是不能赋值给A的! 但是由于我们关闭了逆变检查, 而协变的角度来看, B是A的子类型; 所以没有报错, 当我们打开之后
协变通过, 但逆变不通过, 一样报错!
注意, 对象中的逆变, 必须是property的形式的函数类型才会触发:
strictNullChecks
如果关闭此配置项, 那么, null和undefined可以视作任何类型的子类型! 注意, 它和前面的
exactOptionalPropertyTypes配置项是相互关联的, 因为exactOptionalPropertyTypes开启后是不允许可选参数被赋值undefined, 如果关闭strictNullChecks就意味着可以给任何变量赋值undefined, 这是矛盾的!
let str:string = null
let num:number = undefined
而且, 还会导致一些xx|undefined的问题被忽略
let arr:string[] = ['jack', 'rose', 'boat']
let result = arr.filter(item => item === 'dog')
// 开启strictNullChecks的时候, data类型为string|undefined
// 关闭后, data类型为string
let data = result[0]
// 如果data被误判为string, 则有可能导致这行不会提示错误, 而运行时出错!
data.replace(/a/, 'b')
strictPropertyInitialization
本选项要求, 类中的属性必须被初始化, 哪怕你在构造函数中初始化也行
class Foo {
name:string; // 报错
age:number;
constructor () {
this.age = 12
}
}
skipLibCheck 与 skipDefaultLibCheck
skipLibCheck, 来自不同的声明文件可能会存在冲突, 而开启skipLibCheck的目的就是跳过这些类型检查; skipDefaultLibCheck则是专门针对三斜线引入的默认库, 开启之后, 将不会再引入这些库;
工程类
references
随着我们的项目越来越庞大, 代码越来越多, 我们可能会需要将公共代码拆分出来, 并将业务代码拆分为多个模块; 业内也有很多类似的解决方案, 例如: monorepo, monorepo的好处就是, 可以提升代码的可维护性, 可以提升代码的复用率, 提升开发效率等; 有兴趣的可以去看看相关的资料并学习, 这里只将Typescript中, 类似方案的实现, 这里需要用到一个很重要的配置项: references, 说白了就是关联某个独立的Typescript模块, 假如我们现在有多个独立的Typescript工程, 在同一个仓库中维护:
我们拥有一个main包, 假设就是我们的一个实际运行的工程; 又有assets、hooks、utils等资产或工具, 我们的main会引用它们, 而这些工具之间又有引用关系, 比如: assets就引用了utils; 而且这些工具包拥有独立的tsconfig.json文件, 而且配置也不同, utils的module是commonjs, 其他的均为esxx; 但它们又有公共的ts配置在根目录, 即 tsconfig.base.json;
现在的需求是:
- 这些包互相时,不会因为模块标准的不同而报错;
- 我只构建主包main, 其他的工具包必须跟着一起构建到指定目录;
- 必须保持自己独立配置同时, 能够读取公共配置tsconfig.base.json;
我们来着手处理吧:
首先, 我们在main下的tsconfig.json文件中做如下调整:
{
"extends": "../tsconfig.base.json",
"references": [{
"path": "../utils"
},{
"path": "../hooks"
},{
"path": "../assets"
}],
"compilerOptions": {
"module": "CommonJS",
"target": "ES6",
"declaration": true,
"outDir": "../dist/main",
"rootDir": "./",
"baseUrl": ".",
"composite": true
},
}
使用extends继承基础公共配置; references引用另外三个工具类包的配置; 设置outDir为../dist/main
其次, 另外一些工具类的配置修改如下:
utils:
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"module": "CommonJS",
"target": "ES6",
"declaration": true,
"outDir": "../dist/utils",
"rootDir": "./",
"baseUrl": ".",
"composite": true 此选项必须开启, 声明这是一个子模块, 并能被引用!
},
}
hooks:
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"module": "ESNext",
"target": "ES6",
"declaration": true,
"outDir": "../dist/hooks",
"rootDir": "./",
"baseUrl": ".",
"composite": true 此选项必须开启, 声明这是一个子模块, 并能被引用!
},
}
assets:
{
"extends": "../tsconfig.base.json",
"references": [
{
"path": "../utils" assets引用了utils, 所以这里也要加上references
}
],
"compilerOptions": {
"module": "ESNext",
"target": "ES2017",
"declaration": true,
"outDir": "../dist/assets",
"rootDir": "./",
"baseUrl": ".",
"composite": true 此选项必须开启, 声明这是一个子模块, 并能被引用!
},
}
然后, 当我们要构建的时候, 就应该执行 tsc --build main了, 构建出来的结果如下:
esModuleInterop
通常在开发当中通常都是ESM引入ESM, 而CommonJs引入CommonJs, 但是 总会有混用的时候, 先看下以下这段代码:
// index.ts
import react from 'react'
import { useEffect } from 'react'
import * as all from 'react'
console.log(react)
console.log(useEffect)
console.log(all)
通过编译后, 结果为:
// index.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var react_1 = require("react");
var react_2 = require("react");
var all = require("react");
console.log(react_1.default);
console.log(react_2.useEffect);
console.log(all);
执行node index.js, 之后我们会发现react_1.default这部分显示为undefined; 说白了, 就是CommonJs没有所谓的默认导出这种概念, 所以export default会被转为module.exports.default; 问题是实际上React并没导出这个default属性! 所以, 我们需要使用esModuleInterop, 它可以在代码层面上模拟出module.exports.default:
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
var react_1 = __importDefault(require("react"));
var react_2 = require("react");
var all = __importStar(require("react"));
console.log(react_1.default);
console.log(react_2.useEffect);
console.log(all);
可以看到, 默认导入被加上了一个辅助函数__importDefault, 命名空间导入也被加上了辅助函数__importStar, __importDefault是将所有导出的内容都挂到default属性之上; 而__importStar则是将除了default属性之外的所有属性都挂到导出对象上; 我们再来执行node index.js, 会发现不再是undefined了
以上虽然解决了ESM引入CommonJs的一些兼容问题, 但所有辅助函数都直接写在源码里了, 可以再加上之前介绍过的importHelpers来使用tsLib, 这样编译后的结果就变为:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var react_1 = require("react");
var all = tslib_1.__importStar(require("react"));
console.log(react_1.useEffect);
console.log(all);
allowSyntheticDefaultImports
如果说esModuleInterop是在实际代码层面上为导出对象增加了.default属性, 那么allowSyntheticDefaultImports, 则是在类型层面上模拟出了这个default属性, 让我们默认导入CommonJs模块的内容的时候, 不至于出现类型报错, 如果设置该配置项为false 那么刚才的案例就会出现一个缺少默认导出的错误提示
编译器优化
incremental
增量编译, 设置之后会生成一个.tsbuildinfo, 后续编译, 只会编译修改的部分, 有利于提升编译速度;
watchOptions
watchOptions顾名思义就是当我们使用监听模式--watch的时候的配置项, 这里介绍几个常见的配置项
{
"watchOptions": {
"watchFile": "usefsevents",
"watchDirectory": "usefsevents",
"excludeFiles": ["./utils/**/*"],
"excludeDirectories": ["assets"]
}
}
注意, 这个配置项和我们高频使用的compilerOptions是平级的, 不要把它写在compilerOptions内! watchFile/watchDirectory就是规定对监听范围内的文件/文件夹的监听方式:
- usefsevents: 使用原生方法监听;
- fixedpollinginterval: 以固定频率轮询文件/文件夹
- dynamicpriporitypolling: 对频繁修改的文件高频轮询;
- prioritypollinginterval: 固定间隔的检查每个文件是否发生变化,但使用启发式监听的文件的检查频率要低于非启发式监听的文件。
- useFsEventsOnParentDirectory:采用系统的文件系统的原生事件机制监听修改文件所在的目录,这样修改一个文件实际上监听的是此文件所在的目录都被监听了,如此整个项目的文件监听器将显著减少,但可能导致监听并不准确。
excludeFiles和excludeDirectories则是排除不需要监听的文件/文件夹, 类似于exclude, 此处不再赘述
编译日志类
diagnostics
生成本次编译的诊断信息,比如触发文,处理的所消耗的时间等等
listFiles
罗列出纳入本次编译的文件:
listEmittedFiles项, 则仅仅列出本次输出文件
traceResolution
追踪模块解析路径