发布于 2026-01-06 5 阅读
0

如何避免代码检查?它到底是什么?TSLint 设置 一个配置搞定所有 冲突计数器 未定义 可推断类型 它的意义何在?DEV 全球项目展示挑战赛,由 Mux 呈现:展示你的项目!

如何避免代码检查错误?

这一切究竟是怎么回事?

TSLint 设置

一个配置即可统治一切

冲突应对

不明确的

可推断类型

这么做的意义何在?

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

这篇文章摘自我的博客,记得去看看博客上的最新内容哦😉

作为一名程序员,我想你对自己编写的代码期望很高。代码应该易于阅读理解,以便将来与它交互的人能够轻松理解。这其中也包括编写者自己,例如一年后。当你查看自己以前的代码,却不知道某个片段的作用时,显然说明它写得不好。这就是为什么每个程序员都以构建完美、可扩展的架构和严格的代码风格为目标,力求每一行代码都符合规范。虽然项目的架构和结构极其重要,但并不存在一种适用于所有人的万能方法。因此,本文不会讨论这些内容。我们来谈谈代码风格,更具体地说,是代码检查(linting)

代码检查(Linting)是指分析代码中潜在的编程错误bug、风格错误等的过程。不过,我想您应该已经了解这一点了(或许除了没人关心的严格定义😉)。在现代代码中,编辑器和集成开发环境(IDE)中的代码检查工具(linter)能够通过实时检查帮助您编写更优质的代码。在 JavaScript 开发领域,有一些非常实用的工具,例如ESLintJSLintJSHint。它们提供深度自定义和扩展选项,足以让您为任何项目创建符合代码风格的最佳配置。话虽如此,创建这些配置并非易事,尤其是在您不确定某个规则是否重要的​​情况下。🤔

这一切究竟是怎么回事?

很高兴你问了这个问题。🙃 我想跟你分享一下我的一个故事。一个关于配置代码检查工具以及最终结果的故事。公平地说,我讲这个故事的目的并不是说代码检查工具不好或者很复杂。我只是想分享一个有点意思的小故事。

首先,如果你读过我之前的文章,你可能知道我是TypeScript的忠实拥趸。我几乎所有的 JS 项目都会用到它。这次也不例外。于是,我切换到 TypeScript 并设置了工作目录。由于这个项目是要公开的,我认为代码风格**在这里至关重要。这就是我使用Prettier 的原因。如果你还不了解,Prettier 是一个用于**格式化/美化代码的工具,它不进行代码检查等功能——只是单纯的格式化。所以,它很适合处理字符串、行长度等细节问题。当然,它的功能远不止于此。

然后,我将注意力转向了 TypeScript 配置文件——tsconfig.json。我希望设置尽可能严格的规则,因此启用了一些重要的选项。

  • noImplicitReturns - 确保你的函数在所有可能的情况下都返回值(要么返回值,要么不返回任何值)。

  • 严格模式——这个模式稍微复杂一些。它将其他 5 个选项合并成一个简单的软件包:

    • noImplicitAny - 确保代码中不存在 any 类型。
    • noImplicitThis - 不允许引用任何类型的 this
    • alwaysStrict - 以严格模式解析您的代码并使用“use strict”标志
    • strictNullChecks - 确保您永远不会访问值为 null 的属性。
  • noUnusedLocals - 检查未使用的局部变量

这确实是个不错的选择。这样一来,TypeScript 本身就能保证更高的代码质量。但这对我来说还不够。😅

接下来,我安装了TSLint,它本质上是一个 TypeScript 代码检查工具(带有一些基于类型的额外规则)。而有趣的事情也由此开始……

TSLint 设置

TSLint目前来说已经是一款相当不错的工具了。它内置了大量的代码检查规则(还可以创建自定义规则),一些默认配置(也可以扩展),以及其他更多功能……除此之外,它对语言服务协议的支持最近也得到了改进。这意味着 IDE/代码编辑器能够更好地、更快速地处理更复杂的规则,这当然是件好事。

首先,我从NPM下载了tslint-config-airbnb。这是一个非官方的(并非 Airbnb 官方制作)TSLint 配置,遵循Airbnb 的 JS 风格指南。它能很好地提供最佳的 JS 代码检查规则。它扩展了tslint-eslint-rules(为 TSLint 提供 ESLint 中的规则)和tslint-microsoft-contrib(添加了一些直接来自微软的规则,TSLint 最初就是微软开发的)。除此之外,我还使用了tslint-config-prettier,它禁用了所有可能与我正在使用的 Prettier 冲突的规则。总而言之,这套配置非常实用。而且确实如此。简单明了,无需任何其他配置。

但所有这些规则与`tslint:all`相比都显得微不足道。`tslint:all` 是内置配置,它会启用所有合理的内置规则,而我正是启用了它!🙃 让我们来聊聊最终的结果吧!

一个配置即可统治一切

由于我已经写了一些 TypeScript 代码,所以很快就能感受到变化。没错,红色标记(也就是高亮显示的错误)真的很多!但这对我来说并不陌生(以前也做过类似的代码检查工具配置大改动),于是我兴致勃勃地投入工作。相信我,修复/修改代码的过程充满乐趣,因为你知道它会变得更简洁、更易读(前提是你的配置正确),并且符合某种标准

一开始确实不错。这种严格的配置确保了不会出现未检查的未定义值和其他遗留问题。任何“任意类型”的选项都不能存在😅。每个方法、属性或函数,如果还没有文档,都必须添加相应的文档。更严格的条件检查字母排序以及基于访问修饰符的类成员排序,都为代码提供了额外的结构。这让我确信,我和未来的贡献者都将拥有明确的指导原则。但现实是,如果启用所有规则的配置真的那么好,难道大家不会都直接使用它吗?或者至少应该把它作为默认选项吧?所以,在这些合理规则都满足之后,那些棘手的规则就出现了……

棕羊

冲突应对

综上所述,让我们来看看仅由于启用所有规则配置而出现的最显著的问题和冲突。

数组类型

在 TypeScript 中,你可以用两种方式编写数组类型:数组字面量(`array_literal` string[])或泛型数组类型(`array_type` Array<>)。那么,问题出在哪里呢?问题源于tslint-microsoft-config 的一条名为`prefer-array-literal`的附加规则。它与内置的`array-type`规则冲突。顾名思义,前者推荐使用字面量语法,而后者则根据数组元素的类型复杂度推荐相应的语法。这显然存在冲突。我的解决方法是关闭 `array-type` 规则,这样我就可以使用我更喜欢的数组字面量语法了。

神奇数字

嗯,这可能算不上什么大问题,但确实很麻烦。你听说过“魔法数字”吗?这个术语指的是代码中出现一些没有任何实际意义的数字(没错,这是一种反模式)。请看下面的例子:

for(let i = 0; i < 10; i++) {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

在这里,数字 10 很神奇,它凭空出现,并非每个人都知道它真正的作用(但很明显,它只是让循环迭代 10 次,对吧?)。所以,我们来快速解决这个问题。

const numOfIterations = 10;
for(let i = 0; i < numOfIterations; i++){
    // ...
}
Enter fullscreen mode Exit fullscreen mode

要知道,你的变量命名技巧比我好多了。😅 但说到底,这就是神奇数字背后的奥秘。🙃 需要说明的是,这并不是什么问题——给数字命名,让每个人都知道它们代表什么,这绝对是个好习惯。但在像上面那样的情况下,这可能看起来有点不太直观,不过总的来说,它确实很有用。

不明确的

接下来,我遇到了一些关于未定义值的问题。首先是严格布尔表达式规则。它强制要求在需要布尔值的地方使用真正的布尔值。这是什么意思呢?看看这样的代码:

if(possiblyUndefinedVariable){
    // ...
}
Enter fullscreen mode Exit fullscreen mode

这是检查变量是否未定义的方法,很多 JS/TS 开发者可能都会使用这种方法。但这条规则强制你以更严格的方式编写,例如:

if(possiblyUndefinedVariable !== undefined){
    // ...
}
Enter fullscreen mode Exit fullscreen mode

是的,虽然稍微长了一点,但这是实现相同功能的更明确的语法。

接下来我们来看返回 undefined 的规则。这条规则确保当你的函数原本应该返回其他类型的值时,你会使用 `return undefined` 而不是 `return`。举个简单的例子?

// any type shouldn't be used, but anyway
function returnValue(valueToReturn: any, shouldReturn: boolean){
    if(shouldReturn){
        return valueToReturn;
    }
    return undefined;
}
Enter fullscreen mode Exit fullscreen mode

如你所见,即使并非真正需要,我也必须返回 undefined。另外,需要注意的是,shouldReturn由于它是布尔类型,因此在这里我可以不进行严格的 undefined 检查。

所以,这些规则可能看起来有点反直觉,但它们确实能为你的代码增加一些结构。

导出默认值

你了解 ES 模块吧?TSLint 甚至对它们也有一条规则。我们这里讨论的是no-default-export规则,它与no-default-import 规则配合使用,有效地禁止了任何形式的默认导出和导入。这强制你只导出/导入代码中已命名(赋值给变量)的部分,从而提高代码的可读性和自文档性。但实际上,如果你使用一致的命名规则,仍然可以通过默认导出/导入达到类似的效果

增量和减量

还记得上面提到的循环示例吗?考虑到递增递减规则,它应该被视为存在问题。

const numOfIterations = 10;
for(let i = 0; i < numOfIterations; i++){
    // ...
}
Enter fullscreen mode Exit fullscreen mode

这一切都归功于递增/递减运算符。递增/递减运算符常见于标准的 for 循环中。你可能不知道的是,它既可以放在参数之前,也可以放在参数之后,两种语法含义不同++。请看下面的简单示例:--

let a = 1;
let b = a++;
let c = ++a;
console.log(a,b,c); // 3 1 3
Enter fullscreen mode Exit fullscreen mode

通过了解输出结果,您可以推断出两种语法的含义。使用运算符作为后置字符时,您先将变量a赋值,b然后再加 a 1。另一方面,使用运算符作为前置字符时先将变量的值加a1,然后再将这个值赋给c变量。乍一看,这似乎合乎逻辑,同样的逻辑也适用于递减运算符。

但事实上,这些语法上的细微差别往往会导致各种难以发现的问题。因此,该规则建议使用不同的、更严格的语法:

let a = 1;

let b = a;
a += 1; // a++

a += 1; // ++a
let c = a;

console.log(a,b,c); // 3 1 3
Enter fullscreen mode Exit fullscreen mode

我特意将这些代码行分开,是为了展示如何用不同的语法实现相同的结果。我想我们都同意,这样一来,这段代码背后的思路比上面的语法更容易理解。然而,对于那些喜欢简洁语法的人来说,这条规则可能显得没有必要。

可推断类型

现在,让我们深入探讨更多 TypeScript 特有的规则以及与之相关的问题。这里我们又遇到了一个规则冲突,而且这次的冲突更加严重。我指的是` typedef``no-inferrable-types`之间的冲突。我想这些名称本身就足以说明问题,但我们还是来解释一下这些规则吧。

const myNumber: number = 1;
const myString = "a";
Enter fullscreen mode Exit fullscreen mode

这里有两个简单的变量声明。它们之间有什么区别?(我不在乎类型和名称的差异😛)严格类型定义。TypeScript 能够根据变量的值推断其类型(至少在声明时赋值的情况下)。当然,你可以直接写出具体的类型,但谁会在意这种细节呢?直接写类型感觉不太符合 DRY 原则。

那么,问题出在哪里呢?问题在于这两条规则之间的冲突。第一个声明符合typedef规则的要求(该规则要求所有指定的构造都必须有严格定义的类型),但在no-inferrable-types 规则方面却不太符合(该规则不允许在不需要的地方使用严格定义)。而另一个声明则完全不同。tslint :all配置通过同时启用这两条规则,让你可以自由选择。😁

我对此有何看法?嗯,一开始我考虑过保留 typedef 规则,以求更加严格。但后来我想,这有点过头了。此外,我还遇到了类似这样的情况:

const myArrowFunction = (arg1: number, arg 2: number) => {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

启用 typedef 的决定很容易就做出了。那么,问题出在哪里呢?在上面的例子中,我们使用了箭头函数。如你所知,箭头函数只能像函数表达式一样定义,通过赋值给一个变量来实现(除非你用它创建了立即执行函数表达式)。而 typedef 的要求是什么呢?它要求每个变量都必须直接拥有严格的类型定义。那会是什么样子呢?

const myArrowFunction: (arg1: number, arg 2: number) => void 
= (arg1: number, arg 2: number) => {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

我认为即使是最严苛的人也不会喜欢这种风格。选择很简单。不过,如果你坚持严格要求,可以对typedef 规则进行深度配置,使其仅在特定情况下才要求严格定义。说实话,我认为这种做法会给你的代码带来一些不规范之处。但这只是我个人的看法。

接口

简单提一下。在 TypeScript 中,一种常见的做法是在所有接口名称前加上大写字母I。而且……这还有一条规则!它叫做 ` interface-name`,强制执行这种命名风格。虽然这能清晰地区分接口和其他结构,但至少在我看来,它并不直观。要知道,即使是官方的 TypeScript 库lib.d.ts也没有采用这种做法(也许是为了与其他 JavaScript 文档兼容,但无论如何,这是事实),所以其实并非必须如此。

对于...在&索引签名

这是我最不想抱怨的事。🙃 你有没有遇到过需要遍历对象键值对等的情况?你是怎么做的?我最常用的是for...in循环,这可能是最常用的方法,而且可以说是最快的。

但在介绍问题之前,让我先解释一些 TypeScript 的知识。首先,这个keyof运算符是所谓的索引类型查询运算符,它本质上是创建一个包含对象、接口等所有已知属性的类型。

const obj = {
    a: 1, 
    b: 2
}
type ObjKeys = keyof typeof obj; // "a" | "b"
Enter fullscreen mode Exit fullscreen mode

我觉得这很容易理解。接下来,我们来谈谈索引签名。简单来说,它允许你定义一个给定的类型(例如对象),该类型具有某种类型(通常是字符串)的属性键,并且这些键只能取指定类型的值。所以,它就像是所有属性都必须遵循的通用准则。

interface Obj {
    [key: string]: number;
}
type ObjKeys = keyof Obj; // string
Enter fullscreen mode Exit fullscreen mode

另外,请看一下keyof这个例子中的输出结果。它应该是字符串类型,正如我们之前定义的那样。

既然你已经了解了这一点,那么让我来介绍一下问题。它与keyof类型、索引签名以及诸如 for...in 循环Object.keys() 之类的东西有关。请看下面的例子。

interface Obj {
    a: number;
    b: number;
}
const obj: Obj = {
    a: 1,
    b: 2
}
for(const key in obj){
    if(obj.hasOwnProperty(key)){
        console.log(obj[key]) // error
    }
}
Enter fullscreen mode Exit fullscreen mode

问题在于,我们可以通过键访问类型为 Obj 的对象,因为它没有索引签名!因此,它会返回任何配置不允许的值!为什么呢?因为key键的类型是字符串。所以,问题在于 for...in 循环以及任何其他相关方法(例如Object.keys())都使用字符串而不是类型(在这里类型是更好的选择)来指示键的类型!如何解决这个问题?每次尝试访问值时,keyof都对键的类型进行大小写转换:key

// ...
console.log(obj[key as keyof Obj])
// ...
Enter fullscreen mode Exit fullscreen mode

需要说明的是, GitHub 上已经有很多关于这个特定问题的讨论,但遗憾的是,它们并没有产生太多结果(至少在我使用TS 3.2.2时是这样)。

你可以考虑在循环开始时只对键进行一次类型转换并将其保存到一个变量中,但用两个变量保存相同的值,而且它们的名称很可能相似,这并不是一个好办法。所以,这确实是个大问题

而且……这大概是最后一个最重要的问题了。当然,还有一些其他的小问题,但它们大多与我的编码风格有关,所以我就没在这里列出来。😁

标有问号的森林树木

这么做的意义何在?

正如我之前所说,这篇文章的目的并非劝阻你使用代码检查工具,而是想提醒你配置好代码检查工具的重要性。同时,也希望你能从中获得乐趣,学习一些新知识(比如 TSLint 规则、TypeScript 相关知识等等)。你当然可以使用 ` tslint:all`配置(或者你选择的类似配置),然后禁用不需要的规则。总之,在进行任何类型的项目(尤其是大型项目)时,一定要使用代码检查工具。😉

今天就到这里啦。如果你喜欢这篇文章,欢迎访问我的博客查看最新博文。也欢迎在Twitter🐦Facebook上关注我,获取更多内容。✌

文章来源:https://dev.to/areknawo/how-not-to-lint-your-code-53mh