likes
comments
collection
share

Typescript学习(十七)Typescript中的约束配置

作者站长头像
站长
· 阅读数 14

说到代码的约束, 风格的统一, 我们最容易想到的可能就是eslint和prettier了, eslint可以找出代码中的语法错误, 也可以规范代码风格; prettier则专注于代码格式问题, 它在代码格式上也比eslint做得更加彻底, 更加专业; 我们就来一一介绍他们在Typescript工程中的应用吧

Eslint

安装

在一个已经存在的工程下(即至少有package.json), 执行以下命令都能生成eslint配置文件

# node: 14.0.0
npm init @eslint/config #可全局使用
npx eslint --init #具体项目使用

然后回答生成过程中的问题

Typescript学习(十七)Typescript中的约束配置

则项目根目录会生成配置文件.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, 所以执行以上命令的时候, 除了生成以上配置文件, 还会自动安装以下依赖

Typescript学习(十七)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!

Typescript学习(十七)Typescript中的约束配置

所以此时要手动降低版本

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插件予以解决

Typescript学习(十七)Typescript中的约束配置

error Parsing error: ‘xx‘ expected

这类错误一般需要从eslint的parser配置入手, 即编译器问题, 通常, vue脚手架生成的配置一般都会定义好parser, 例如, 前面的vue3默认配置中, 虽然没有parser这一项, 但是, 在'plugin:vue/vue3-essential'的源码中, 其实也是配置好了parser的, 所以脚手架生成的一般不会出现此问题

Typescript学习(十七)Typescript中的约束配置

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则负责代码风格检查;

Typescript学习(十七)Typescript中的约束配置这样, 所有的格式问题, 也由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件事:

  1. 在package.json的依赖对象中添加husky, 但是没有真正安装husky, 所以后面都要走一次安装依赖的命令;
  2. 根目录下创建.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 的时候, 就会校验对应的代码文件, 并尝试修复;

Typescript学习(十七)Typescript中的约束配置

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所在路径, 否则报错失效!

Typescript学习(十七)Typescript中的约束配置

所以, 如果配置了这个插件, 还必须指定parserOptions.project:

// .eslintrc.js
parserOptions: {
  project: './tsconfig.json',
},

注意, 这还没完, 如果你在.eslintrc.js中指定了tsconfig.json, 那么,在这个tsconfig.json中, 你还必须是include了这个.eslintrc.js文件的,否则还是报错!

Typescript学习(十七)Typescript中的约束配置

所以, 我们必须保证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
评论
请登录