Typescript学习(十七)Typescript中的约束配置
说到代码的约束, 风格的统一, 我们最容易想到的可能就是eslint和prettier了, eslint可以找出代码中的语法错误, 也可以规范代码风格; prettier则专注于代码格式问题, 它在代码格式上也比eslint做得更加彻底, 更加专业; 我们就来一一介绍他们在Typescript工程中的应用吧
Eslint
安装
在一个已经存在的工程下(即至少有package.json), 执行以下命令都能生成eslint配置文件
# node: 14.0.0
npm init @eslint/config #可全局使用
npx eslint --init #具体项目使用
然后回答生成过程中的问题
则项目根目录会生成配置文件.eslintrc.js
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"overrides": [
{
"env": {
"node": true
},
"files": [
".eslintrc.{js,cjs}"
],
"parserOptions": {
"sourceType": "script"
}
}
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
}
}
因为我们选择了Typescript, 所以执行以上命令的时候, 除了生成以上配置文件, 还会自动安装以下依赖
如果你是在一个空文件夹下执行该命令, 即没有package.json等任何文件, 那你执行之后, 只会生成一个配置文件, 依赖eslint、@typescript-eslint/eslint-plugin、@typescript-eslint/parser等都要重新手动安装一次
npm i eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser
然后在package.json中增加入如下scripts
// ...
"scripts": {
"eslint": "eslint './src/**/*.{js,jsx,ts,tsx}' --cache",
"eslint:fix": "npm run eslint -- --fix"
}
// ...
此时我们可以执行以下命令来帮助我们查找并修复eslint报错
npm run eslint:fix
这里要注意: typescript的版本和@typescript-eslint/xx的版本容易出现不兼容, 例如: 本文撰写的时候, @typescript-eslint/eslint-plugin最新正式版本为5.60.0而typescript的最新正式版本为5.1.3, 但是此时的@typescript-eslint/typescript-estree支持的typescript的版本号为>=3.3.1 <5.1.0!
所以此时要手动降低版本
npm i typescript@5.0.4
然后执行 npm run eslint:fix 就能对代码进行校验, 并修一些简单的错误了;
rules
完成配置文件的生成之后, 此时我们就有了基础的代码规则, 但是, 如果要对现有的规则进行修改
// ...
"rules": {
'indent': 'off',
'@typescript-eslint/indent': ['error', 2],
'quotes': 'off',
'@typescript-eslint/quotes': ['error', 'single'],
'semi': 'off',
'@typescript-eslint/semi': ['error', 'never']
}
// ...
我们会发现, 每个@typescript-eslint/xx的配置之上, 都会有一个xx: 'off', 这其实是把该配置的基础配置给关闭; @typescript-eslint/xx的值一般是一个数组, 其第一元素是警告的程度, 即 error、warn、off, 分别表示报错、警告、关闭; 第二元素代表具体的规则, 比如: indent的2, 代表2个tab, quotes的'single'代表单引号, semi的'never'代表永远不要分号;
.eslintignore
和.gitignore一样, 总有些文件是不需要eslint校验的, 所以, 可以在项目根目录上创建.eslintignore文件, 将不需要校验的文件后缀加入
*.css
*.svg
*.html
*.json
Vue3中的常见问题
vue3的默认配置(V3.3.4)
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
}
}
通常, 为了编辑器能够给出提示, 通常只需要在这个基础上加上plugin:prettier/recommended即可
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting',
'plugin:prettier/recommended'
],
parserOptions: {
ecmaVersion: 'latest'
}
}
Parsing error: 'import' and 'export' may appear only with 'sourceType: module'
有时, 在.ts文件中, import上会出现以下错误提示:
Parsing error: 'import' and 'export' may appear only with 'sourceType: module'
这个时候可以将eslint配置文件中的sourceType设置为'module'
找不到模块“./App.vue”或其相应的类型声明
在vscode中可能出现此报错, 可以安装TypeScript Vue Plugin插件予以解决
error Parsing error: ‘xx‘ expected
这类错误一般需要从eslint的parser配置入手, 即编译器问题, 通常, vue脚手架生成的配置一般都会定义好parser, 例如, 前面的vue3默认配置中, 虽然没有parser这一项, 但是, 在'plugin:vue/vue3-essential'的源码中, 其实也是配置好了parser的, 所以脚手架生成的一般不会出现此问题
Prettier
介绍完了Eslint, 我们再来看看Prettier, 和Eslint相比, Prettier更加重视代码风格, 或者说, 只专注于代码风格; 而Eslint则更加关注于代码质量问题; 既然如此, 那我们要做的应该是物尽其用, 即 让Eslint负责代码质量, 让Prettier负责代码风格; 但是我们知道, Eslint偏偏也管了一部分代码风格, 那怎么办呢? 我们要将Eslint的这部分代码风格相关的规则禁用掉! 好了, 说了那么多, 可以动手操作了
安装
老规矩, 还是先安装依赖
npm i prettier eslint-config-prettier eslint-plugin-prettier -D
关于这几个依赖, 我们一个个认识下:
prettier 不用解释了, 最基本的依赖, 没有这个啥也干不了;
eslint-config-prettier 前面不是说要把Eslint中, 代码风格部分的配置禁用掉吗?这个插件可以帮助我们; eslint-plugin-prettier 当我们禁用Eslint的代码风格配置后, 整个项目的代码风格已经全面由Prettier接管了, 那么这个插件的作用就是将Prettier的规则, 以Eslint的rules的形式注入Eslint中, 这样, 所有错误都由Eslint统一由Eslint报出;
我们先来配置eslint-config-prettier
// .eslintrc.js
// ...
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier' // 增加这行, 一定要放在最后
]
// ...
然后是eslint-plugin-prettier
// .eslintrc.js
// ...
plugins: ['@typescript-eslint', 'prettier'],
rules: {
'prettier/prettier': 'error',
}
// ...
说了, 我们是将prettier以Eslint插件的形式传入; 以上的配置合起来大概就是这样:
// ...
plugins: ['prettier'],
rules: {
'prettier/prettier': 'error',
},
extends: [
'prettier'
]
// ...
上面的代码可以简化为一行:
extends: [
'plugin:prettier/recommended',
]
至此 ,我们完成了一件事, 那就是对Eslint和Prettier进行了分工: Eslint负责代码质量和错误提示; Prettier则负责代码风格检查;
这样, 所有的格式问题, 也由eslint抛出, 但是实际起作用的, 是prettier
配置
分完工之后, Prettier还要有具体的配置, 同样在根目录新建配置文件.prettierrc.js文件
module.exports = {
// 单行最多 80 字符
printWidth: 80,
// 一个 Tab 缩进 2 个空格
tabWidth: 2,
// 每一行结尾需要有分号
semi: true,
// 使用单引号
singleQuote: true,
// 在对象属性中,仅在必要时才使用引号,如 "prop-foo"
quoteProps: 'as-needed',
// 在 jsx 中使用双引号
jsxSingleQuote: false,
// 使用 es5 风格的尾缀逗号,即数组和对象的最后一项成员后也需要逗号
trailingComma: 'es5',
// 大括号内首尾需要空格
bracketSpacing: true,
// HTML 标签(以及 JSX,Vue 模板等)的反尖括号 > 需要换行
bracketSameLine: false,
// 箭头函数仅有一个参数时也需要括号,如 (arg) => {}
// 使用 crlf 作为换行符
endOfLine: 'crlf',
};
这里, 就是整个项目代码风格的配置文件了;
当然, 我们并不是要对所有文件的代码风格都进行校验, 此时就需要用到.prettierignore
添加命令
配置搞定之后, 我们还要给package.json中的scripts添加对应的校验命令
"scripts": {
"eslint": "eslint './src/**/*.{js,jsx,ts,tsx}' --cache",
"eslint:fix": "npm run eslint -- --fix",
"prettier": "npx prettier --check .",
"prettier:fix": "npx prettier --write .",
"lint": "npm run eslint && npm run prettier",
"lint:fix": "npm run eslint:fix && npm run prettier:fix"
},
prettier仅仅是检查, prettier:fix则是修复, 并且, 我们可以将eslint和prettier两个工具合起来使用, lint表示先执行eslint检查后执行prettier检查; lint:fix则是先后执行两者的修复命令;
Git Hook
虽然我们已经通过多种手段限制了代码格式, 现在只要执行以下npm run lint:fix, 就能够修复代码格式问题; 但是, 这仍然不能保证团队中每个人都会照做, 即使命令再简单, 也会有不愿或者忘记去执行的人, 那么怎么办?这时候就需要用到git hooks, 这样, 我们就可以在pre-commit, 即 commit之前, 强行进行格式校验, 如果格式不通过, 就不会提交!
安装
# npm
npx husky-init && npm install
# yarn1+
npx husky-init && yarn
# yarn2+
pnpm dlx husky-init --yarn2 && yarn
# pnpm
pnpm dlx husky-init && pnpm install
注意, 无论是npx husky-init 还是pnpm dlx husky-init, 它们都只做了2件事:
- 在package.json的依赖对象中添加husky, 但是没有真正安装husky, 所以后面都要走一次安装依赖的命令;
- 根目录下创建.husky文件夹;
这样, 把husky安装好, .husky文件生成好之后, 就要开始生成钩子要执行的命令了, 我们的初衷是: commit之前, 对暂存区代码进行校验, 那我们怎么找到暂存区代码呢? 这就要用到lint-staged了, 它能够找出放入暂存区的文件, 然后执行校验;
我们先安装lint-staged
npm i lint-staged -D
pnpm i lint-staged -D
yarn add lint-staged -D
添加钩子
安装好所有依赖后, 接下来就要生成钩子命令
npx husky add .husky/pre-commit './node_modules/.bin/lint-staged'
这样, 在.husky/pre-commit中就会生成一段代码
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
./node_modules/.bin/lint-staged # 新增
我们的目的就是要执行lint-staged命令
添加配置
添加完命令之后, 我们还要让lint-staged知道, 要校验那些文件, 怎么校验, 执行什么命令, 这些可以在package.json根目录中直接配置:
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --cache --fix",
"prettier --write --list-different"
],
"*.{html,md,json,css,scss,less}": [
"prettier --write --list-different"
]
}
注意, 这里的eslint --cache --fix, cache指的是开启缓存机制, 即 如果上次执行了校验, 且没有修改, 则不再重复校验, 可以提高校验的效率; fix则指的是修复; prettier --write --list-different中的--list-different指的是列出有问题的文件; 这样, 我们在执行git commit 的时候, 就会校验对应的代码文件, 并尝试修复;
Eslint 规则推荐
常规限制
Typescript中, 有很多存在多种写法的语法, 比如, 表示数组, 可以用number[], 也可以用Array; 断言, 可以用 data as number, 也可以写成 data; 这些语法, 可以说都没错, 但是, 如果总是混用, 代码也会显得颇为混乱, 所以需要订立规则, 来约束;
array-type
主要用于规定数组类型的表示方式
// .eslintrc.js
// ...
rules: {
'@typescript-eslint/array-type': 'error'
}
// ...
await-thenable
很多时候, 我们会滥用await , 哪怕是毫无意义地使用, 比如:
const fn = () => 'value';
async function awaitFn() {
await fn();
await 'hehe';
}
awaitFn();
我们可以看到, 无论是函数fn还是字符串'hehe', 压根都不需要使用到await! 如果代码复杂起来, 这显然会误导后续的开发者, 误认为这里是一个异步的方法! 所以就需要使用到await-thenable;
rules: {
'@typescript-eslint/await-thenable': 'error',
},
这样, 以上写法必须改为:
const fn = () => Promise.resolve('value');
async function awaitFn() {
await fn();
await Promise.resolve('hehe');
}
另外, 还要注意的是, 这个插件, 非常特殊, 它必须要指定parserOptions.project, 即 tsconfig.json所在路径, 否则报错失效!
所以, 如果配置了这个插件, 还必须指定parserOptions.project:
// .eslintrc.js
parserOptions: {
project: './tsconfig.json',
},
注意, 这还没完, 如果你在.eslintrc.js中指定了tsconfig.json, 那么,在这个tsconfig.json中, 你还必须是include了这个.eslintrc.js文件的,否则还是报错!
所以, 我们必须保证tsconfig.json中的include了这个文件, tsconfig.json:
{
"include": ["./.eslintrc.js", "./src/**/*.ts"],
}
consistent-type-assertions
用于规范断言的格式, 我们知道Typescript中, 断言的写法有两种
type obj = { name:string }
const a = {} as obj
const b = <obj>{}
明显, a和b都是正确的, 为了统一风格, 就需要用到consistent-type-assertions
rules: {
"@typescript-eslint/consistent-type-assertions": "error"
}
这样, 就只能使用as语法了
consistent-type-definitions
这条规则, 从名字就可以看出, 和consistent-type-assertions类似, consistent-type-assertions是要求断言必须用统一的风格; 而consistent-type-definitions指的是定义必须是统一的风格! 定义什么? 定义的就是对象类型; 通常情况下, 我们可以用interface也可以用type来声明一个对象类型;
// 以下代码均为正确
interface IterfaceObj {
name:string;
}
type TypeObj = {name:string}
但这样可能也会显得很乱, consistent-type-definitions正是为解决这个问题而生的:
rules: {
"@typescript-eslint/consistent-type-definitions": "error"
}
这样, 对象的类型, 就只能用interface来声明了, 但是可能会有人觉得, type定义对象更好, 针对这个需求, 可以传入第二个参数作为配置项
rules: {
"@typescript-eslint/consistent-type-definitions": ["error", "type"]
}
这样, 则是只有type才能用来声明对象类型了
naming-convention
如果我们统一用interface来声明一个对象, 但是, 名字风格五花八门, 也会给人一种很不专业的感觉
// 以下代码也都没问题
interface nameObj {
name:string;
}
interface name_obj {
name:string;
}
interface NameObj {
name:string;
}
此时, 我们就需要用到naming-convention
rules: {
"@typescript-eslint/naming-convention": "error"
}
这样, 所有的接口名都必须是PascalCase, 即首字母大写
interface NameObj {
name:string;
}
如果不喜欢这种风格, 例如, 想要用驼峰(camelCase)写法, 可以增加配置format选项
rules: {
"@typescript-eslint/naming-convention": [
"error",
{
"format": ['camelCase'],
"selector": "interface"
}
]
}
注意, 想要定义format, 必须同时定义好selector, 否则无效! selector则是指你要将这套规则作用到哪种关键字上, 它还可以是一个数组, 接受更多的关键字, 比如, 我们希望函数名也符合这种规则: ["interface", "function"]; 当然, 有时候我们还想自定义规则, 比如, 我想让所有接口名, 都必须是大写字母I开头
rules: {
"@typescript-eslint/naming-convention": ["error",
{
"format": ['PascalCase'],
"custom": {
"regex": "^I[A-Za-z]",
"match": true
},
"selector": ["interface"]
}]
}
我们增加了一个custom属性, 其中regex就是一个正则, match为true则是指, 必须符合regex的正则; 为false则是指取反逻辑, 即不能符合regex! 这个插件的内容较多, 更多内容可以参考文档
prefer-for-of
有的时候, 我们都会使用for循环来对数组中的某个成员进行操作, 但是其实for..of会更合适, 更简洁些
// 以下代码均正确!
declare const arr:[]
for (let i = 0; i < arr.length; i++) {
console.log(arr[i])
}
for (const item of arr) {
console.log(item)
}
配置prefer-for-of来将统一循环操作
rules: {
"@typescript-eslint/prefer-for-of": "error"
}
这样, 前面案例中的for循环就会提示错误, 当然, 如果在for循环中, 我们不仅用了单个元素, 还用了诸如下标等其他东西, 那么即使本规则生效, for循环也不会报错, 比如, 我们将上面案例修改下, 规则生效的时候, 也不会报错:
declare const arr:[]
for (let i = 0; i < arr.length; i++) {
console.log(arr[i], i)
}
所以, 此规则近用于循环遍历时, 仅使用当前元素的for循环!
prefer-nullish-coalescing
在ES2020中, js新增了nullish合并操作符??, 在这之前, 我们如果想给一个值设置默认值, 通常使用或运算符||:
type NullOrUndefined<T> = T | null | undefined;
let data: NullOrUndefined<number>;
const bool = data || 'default value';
但是这有个问题, 那就是||左侧的值会被隐式转换, 本案例中, data可能为0, 那么data最终会取到value的值, 因为0会被隐式转为false, 就取到了右侧的'default value', 但有时候, 我们可能就是要0, 这就造成了一定的麻烦, 所以, nullish合并操作符应运而生, 它不会对运算符左侧进行隐式转换, 并且左侧只有是null或者undefined的时候, 才会取右侧的值
type NullOrUndefined<T> = T | null | undefined;
let data: NullOrUndefined<number>;
const bool = data ?? 'default value';
这就是??的作用, 但是日常开发中, 仍然会有开发者习惯于使用||来应付以上场景, 所以需要通过配置prefer-nullish-coalescing来限制这种写法:
rules: {
'@typescript-eslint/prefer-nullish-coalescing': 'error',
},
值得注意的是, 这个规则必须在tsconfig.json中, strictNullChecks为true的时候才能生效
prefer-optional-chain
说完了nullish合并操作符??, 再来看看可选链?. , 通常, 我们为了防止空指针, 会采用短路运算符&&, 来确保不出现空指针错误
interface IObj {
info?: {
name?: string;
};
}
function fn(obj: IObj) {
const data = obj && obj.info && obj.info.name;
return data;
}
有了可选链之后, 我们就可以这样写了
function fn(obj: IObj) {
const data = obj?.info?.name;
return data;
}
为了禁止继续使用短路运算符, 可以增加prefer-optional-chain配置
rules: {
'@typescript-eslint/prefer-optional-chain': 'error',
},
转载自:https://juejin.cn/post/7270532002733998116