ESLint 和 TypeScript 之间的差异

诸如 ESLint 这样的 Linters 和诸如 TypeScript 这样的类型检查器可以捕获不同领域的代码缺陷,最好结合使用。

如果您是当今的 JavaScript 开发者,您很有可能正在使用 ESLint 和 TypeScript 的组合来辅助开发。这些工具执行相似但不同的功能。ESLint 是一个linter(代码检查工具),而 TypeScript 是一个type checker(类型检查器)。

Linters 和类型检查器是两种静态分析工具,它们分析代码并报告检测到的问题。虽然它们乍一看可能很相似,但 linters 和类型检查器检测到的问题类别不同,用途也不同。

为了理解这些差异,首先了解什么是静态分析以及它为什么有用是很有帮助的。

什么是静态分析?

静态分析是指在不执行源代码的情况下检查源代码。这与动态分析不同,在动态分析中,源代码在执行时被检查。因此,动态分析带来了执行恶意代码或在静态分析安全执行时产生副作用的内在危险,而与源代码无关。

静态分析对于提高代码可读性、可靠性和整体质量非常有帮助。许多开发者依赖静态分析来强制执行一致的代码格式和风格,确保代码文档齐全,并捕获可能的错误。由于静态分析在源代码上运行,因此它可以在编写代码时在编辑器中建议改进。

ESLint 的静态分析组织为一系列单独配置的 lint 规则。由于没有两条规则相互作用,您可以根据自己的偏好安全地打开和关闭每条规则。虽然 TypeScript 有一些单独配置的选项,但大部分分析都在类型检查功能中执行。

ESLint 和 TypeScript 使用一些相同的分析形式来检测代码中的缺陷。它们都分析代码中作用域和变量的创建和使用方式,并且可以捕获诸如引用不存在的变量等问题。我们将探讨两者使用分析代码信息的不同方式。

深入探讨 linting 与类型检查

Linters 主要报告可能的缺陷,也可以用于强制执行主观意见。ESLint 和其他 linters 可以捕获可能类型安全也可能类型不安全但可能是错误来源的问题。许多开发者依赖 linters 来确保他们的代码遵循框架和语言的最佳实践。

例如,开发者有时会在 switch 语句的 case 的末尾遗漏 breakreturn。这样做是类型安全的,并且 JavaScript 和 TypeScript 都允许。但在实践中,这几乎总是一个错误,会导致下一个 case 语句意外运行。ESLint 的 no-fallthrough 可以捕获这个可能的错误。

function logFruit(value: "apple" | "banana" | "cherry") {
switch (value) {
case "apple":
console.log("🍏");
break;

case "banana":
console.log("🍌");

// eslint(no-fallthrough):
// Expected a 'break' statement before 'case'.

case "cherry":
console.log("🍒");
break;
}
}

// Logs:
// 🍌
// 🍒
logFruit("banana");

另一方面,类型检查器确保值仅以值类型允许的方式使用。像 Java 这样的编译语言在编译阶段执行类型检查。由于 JavaScript 无法指示绑定的预期类型,因此它无法自行执行类型检查。这就是 TypeScript 的用武之地。

通过允许显式类型注解(以及隐式检测某些类型),TypeScript 将类型信息覆盖在 JavaScript 代码之上,以执行类似于编译语言中的类型检查。例如,TypeScript 在以下 logUppercase(9001) 调用中报告类型错误,因为 logUppercase 被声明为接收 string 而不是 number

function logUppercase(text: string) {
console.log(text.toUpperCase());
}

logUppercase(9001);
// ~~~~
// Argument of type 'number' is not assignable to parameter of type 'string'.

TypeScript 专注于报告已知错误而不是潜在问题;TypeScript 报告的错误没有任何主观性,也没有办法实现项目特定的偏好设置。

看待 ESLint 和 TypeScript 之间差异的另一种方式是,TypeScript 强制执行你可以做什么,而 ESLint 强制执行你应该做什么。

细粒度的可扩展性

ESLint 和 TypeScript 之间的另一个区别在于配置的粒度。

ESLint 运行一组单独可配置的 lint 规则。如果您不喜欢特定的 lint 规则,您可以为一行、一组文件或整个项目关闭它。ESLint 还可以通过插件进行增强,插件可以添加新的 lint 规则。插件特定的 lint 规则扩展了 ESLint 配置可以从中选择的代码检查的广度。

例如,此 ESLint 配置启用了 eslint-plugin-jsx-a11y 中的推荐规则,该插件为使用 JSX 库(如 Solid.js 和 React)的项目添加了可访问性检查。

import js from "@eslint/js";
import jsxA11y from "eslint-plugin-jsx-a11y"

export default [
js.configs.recommended,
jsxA11y.flatConfigs.recommended,
// ...
];

然后,使用 JSX 可访问性规则的项目将被告知其代码是否违反了常见的可访问性指南。例如,渲染没有描述性文本的本机 <img> 标签将收到来自 jsx-a11y/alt-text 的报告。

const MyComponent = () => <img src="source.webp" />;
// ~~~~~~~~~~~~~~~~~~~~~~~~~
// eslint(jsx-a11y/alt-text):
// img elements must have an alt prop, either with meaningful text, or an empty string for decorative images.

通过从插件中添加规则,ESLint 配置可以根据项目构建所使用的框架的特定最佳实践和常见问题进行定制。

另一方面,TypeScript 通过项目级别的一组编译器选项进行配置。tsconfig.json 文件允许您设置编译器选项,这些选项会更改项目中所有文件的类型检查。这些编译器选项是为 TypeScript 全局设置的,通常会更改大范围的类型检查行为。TypeScript 不允许在单个项目的不同文件中使用不同的编译器选项。

重叠领域

虽然 ESLint 和 TypeScript 的操作方式不同,并且专注于不同领域的代码缺陷,但它们之间存在一些重叠。特定类型的代码缺陷跨越了“最佳实践”和“类型安全”之间的界限,因此可以被这两种工具捕获。

我们建议在您的 TypeScript 项目中同时使用 ESLint 和 TypeScript,以确保您捕获最广泛数量和类型的缺陷。以下是一些入门步骤:

注意: typescript-eslint 的 tseslint.configs.recommended 禁用了核心 ESLint 规则,这些规则对于 TypeScript 没有帮助。该配置保留了任何与类型检查一起使用的核心 ESLint 规则。

未使用的局部变量和参数

当使用 linting 时,我们建议关闭的唯一 TypeScript 编译器选项是那些启用检查未使用变量的选项

当不使用 ESLint 时,这些编译器选项很有用。但是,它们不像 lint 规则那样可配置,因此无法根据项目的偏好配置为更高或更低的严格级别。例如,编译器选项被硬编码为始终忽略任何名称以 _ 开头的变量,而 ESLint 的 no-unused-vars 在配置为这样做之前不会以任何不同的方式对待这些变量。

例如,以下 registerCallback 函数为其回调声明了两个参数 idmessage,但使用它的开发者只需要 message。TypeScript 的 noUnusedParameters 编译器选项不会标记未使用的参数 _

type Callback = (id: string, message: string) => void;

declare function registerCallback(callback: Callback): void;

// We only want to log message, not id
registerCallback((_, message) => console.log(message));

JavaScript 中未使用的变量也可以被 ESLint 的 no-unused-vars 规则捕获;在 TypeScript 代码中,首选 @typescript-eslint/no-unused-vars。lint 规则可以配置为忽略名称以 _ 开头的变量。

此外,lint 规则默认忽略在任何自身被使用的参数之前的参数。一些项目更喜欢永远不允许未使用的参数,无论名称或位置如何。这些更严格的偏好有助于防止 API 设计导致开发者创建许多未使用的参数。

更严格的 ESLint 配置将能够报告 _ 参数

/* eslint @typescript-eslint/no-unused-vars: ["error", { "args": "all", "argsIgnorePattern": "" }] */

type Callback = (id: string, message: string) => void;

declare function registerCallback(callback: Callback): void;

// We only want to log message, not id
registerCallback((_, message) => console.log(message));
// ~
// eslint(@typescript-eslint/no-unused-vars):
// '_' is declared but never used.

no-unused-vars 规则提供的额外可配置性使其能够充当与其等效的 TypeScript 编译器选项的更细粒度可配置版本。

💡 请参阅 no-unused-binary-expressions:从代码审查的细枝末节到生态系统改进,了解 linting 和类型检查之间存在部分重叠的更多代码检查领域。

ESLint 在 TypeScript 中有用吗?

是的。

如果您正在使用 TypeScript,那么使用 ESLint 仍然非常有用。事实上,ESLint 和 TypeScript 在结合使用时功能最强大。

带有类型信息的 ESLint

传统的 ESLint 规则一次在一个文件上运行,并且不知道项目中的其他文件。它们不能根据其他文件的内容对文件做出决策。

但是,如果您的项目是使用 TypeScript 设置的,您可以选择加入“类型检查” lint 规则:可以提取类型信息的规则。通过这样做,类型检查的 lint 规则可以根据其他文件做出决策。

例如,@typescript-eslint/no-for-in-array 能够检测到对数组类型值进行 for...in 循环,即使这些值来自其他文件。TypeScript 不会报告对数组进行 for...in 循环的类型错误,因为这样做在技术上是类型安全的,并且可能是开发者想要的。但是,linter 可以配置为注意到开发者可能犯了一个错误,并且本意是使用 for...of 循环。

// declare function getArrayOfNames(): string[];
import { getArrayOfNames } from "./my-names";

for (const name in getArrayOfNames()) {
// eslint(@typescript-eslint/no-for-in-array):
// For-in loops over arrays skips holes, returns indices as strings,
// and may visit the prototype chain or other enumerable properties.
// Use a more robust iteration method such as for-of or array.forEach instead.
console.log(name);
}

类型化的 linting 的代价是使 linting 速度减慢到与类型检查大致相同的速度,但提供了一组更强大的 lint 规则。有关使用 typescript-eslint 进行类型化 linting 的更多详细信息,请参阅 类型化 Linting:有史以来最强大的 TypeScript Linting

带有 linting 的 TypeScript

TypeScript 为 JavaScript 增加了额外的复杂性。这种复杂性通常是值得的,但任何增加的复杂性都带来了误用的可能性。ESLint 对于阻止开发者在代码中犯 TypeScript 特有的错误很有用。

例如,TypeScript 的 {}(“空对象”)类型经常被刚接触 TypeScript 的开发者误用。它在视觉上看起来应该意味着任何 object,但实际上意味着任何非 null、非 undefined 值——包括诸如 numberstring 之类的原始类型。@typescript-eslint/no-empty-object-type 捕获了 {} 类型的用法,这些用法可能意味着 objectunknown

export function logObjectEntries(value: {}) {
// ~~
// eslint(@typescript-eslint/no-empty-object-type):
// The `{}` ("empty object") type allows any non-nullish value, including literals like `0` and `""`.
// - If that's what you want, disable this lint rule with an inline comment or configure the 'allowObjectTypes' rule option.
// - If you want a type meaning "any object", you probably want `object` instead.
// - If you want a type meaning "any value", you probably want `unknown` instead.
console.log(Object.entries(value));
}

logObjectEntries(0); // No type error!

使用 ESLint 强制执行特定于语言的最佳实践有助于开发者学习和正确使用 TypeScript。

结论

诸如 ESLint 这样的 Linters 和诸如 TypeScript 这样的类型检查器对于开发者来说都是宝贵的资产。两者捕获不同领域的代码缺陷,并且在可配置性和可扩展性方面具有不同的理念。

  • ESLint 检查代码是否符合最佳实践并保持一致,强制执行您应该编写的内容。
  • TypeScript 检查代码是否“类型安全”,强制执行您可以编写的内容。

总而言之,这两种工具可以帮助项目编写错误更少、一致性更高的代码。我们建议任何使用 TypeScript 的项目都额外使用 ESLint。

最新的 ESLint 新闻、案例研究、教程和资源。

Evolving flat config with extends
5 分钟阅读

使用 extends 进化扁平化配置

您的 eslint.config.js 文件现在可以使用 extends 来简化您的配置。

ESLint v9.22.0 released
1 分钟阅读

ESLint v9.22.0 发布

我们刚刚推送了 ESLint v9.22.0,这是一个 ESLint 的次要版本升级。此版本添加了一些新功能并修复了先前版本中发现的几个错误。

ESLint v9.21.0 released
2 分钟阅读

ESLint v9.21.0 发布

我们刚刚推送了 ESLint v9.21.0,这是一个 ESLint 的次要版本升级。此版本添加了一些新功能并修复了先前版本中发现的几个错误。