TypeScript 声称是一种构建在 JavaScript 之上的强类型编程语言,可以在任何规模上提供更好的工具。然而, TypeScript包含any
类型,它通常会隐式地潜入代码库并导致失去 TypeScript 的许多优点。
本文探讨了控制 TypeScript 项目中的any
类型的方法。准备好释放 TypeScript 的力量,实现最终的类型安全并提高代码质量。
TypeScript 提供了一系列附加工具来增强开发人员体验和生产力:
但是,一旦您开始在代码库中使用any
类型,您就会失去上面列出的所有好处。 any
类型是类型系统中的一个危险漏洞,使用它会禁用所有类型检查功能以及依赖于类型检查的所有工具。结果,TypeScript 的所有好处都消失了:错误被遗漏,代码编辑器变得不太有用等等。
例如,考虑以下示例:
function parse(data: any) { return data.split(''); } // Case 1 const res1 = parse(42); // ^ TypeError: data.split is not a function // Case 2 const res2 = parse('hello'); // ^ any
在上面的代码中:
parse
函数内的自动完成功能。当您输入data.
在您的编辑器中,您不会获得有关data
的可用方法的正确建议。TypeError: data.split is not a function
错误,因为我们传递了数字而不是字符串。 TypeScript 无法突出显示错误,因为any
会禁用类型检查。res2
变量也具有any
类型。这意味着any
一次使用都会对代码库的大部分产生级联效应。
仅在极端情况或出于原型设计需要时才可以使用any
。一般来说,最好避免使用any
来充分利用 TypeScript。
了解代码库中any
类型的来源非常重要,因为显式编写any
并不是唯一的选择。尽管我们尽最大努力避免使用any
类型,但它有时会隐式地潜入代码库。
代码库中的any
类型有四个主要来源:
any
。
对于前两点,我已经写过关于tsconfig 中的关键注意事项和改进标准库类型的文章。如果您想提高项目中的类型安全性,请检查它们。
这次,我们将重点关注用于控制代码库中any
类型的外观的自动工具。
ESLint是一种流行的静态分析工具,Web 开发人员使用它来确保最佳实践和代码格式化。它可用于强制编码风格并查找不遵守某些准则的代码。
由于typesctipt-eslint插件,ESLint 还可以与 TypeScript 项目一起使用。最有可能的是,这个插件已经安装在您的项目中。但如果没有,您可以按照官方入门指南进行操作。
typescript-eslint
最常见的配置如下:
module.exports = { extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', root: true, };
此配置使eslint
能够在语法级别理解 TypeScript,从而允许您编写适用于代码中手动编写的类型的简单 eslint 规则。例如,您可以禁止显式使用any
.
recommended
预设包含一组精心挑选的 ESLint 规则,旨在提高代码正确性。虽然建议使用整个预设,但出于本文的目的,我们将仅关注no-explicit-any
规则。
TypeScript 的严格模式阻止使用隐含的any
,但它不会阻止显式使用any
。 no-explicit-any
规则有助于禁止在代码库中的任何位置手动编写any
。
// ❌ Incorrect function loadPokemons(): any {} // ✅ Correct function loadPokemons(): unknown {} // ❌ Incorrect function parsePokemons(data: Response<any>): Array<Pokemon> {} // ✅ Correct function parsePokemons(data: Response<unknown>): Array<Pokemon> {} // ❌ Incorrect function reverse<T extends Array<any>>(array: T): T {} // ✅ Correct function reverse<T extends Array<unknown>>(array: T): T {}
该规则的主要目的是防止在整个团队中使用any
规则。这是加强团队一致意见的一种手段,即不鼓励在项目中使用any
。
这是一个至关重要的目标,因为即使是单次使用any
也会由于类型推断而对代码库的很大一部分产生级联影响。然而,这距离实现最终的类型安全还很远。
尽管我们已经处理了显式使用的any
,但项目的依赖项中仍然存在许多隐含的any
,包括 npm 包和 TypeScript 的标准库。
考虑以下代码,它可能在任何项目中看到:
const response = await fetch('https://pokeapi.co/api/v2/pokemon'); const pokemons = await response.json(); // ^? any const settings = JSON.parse(localStorage.getItem('user-settings')); // ^? any
变量pokemons
和settings
都隐式指定为any
类型。在这种情况下no-explicit-any
和 TypeScript 的严格模式都不会警告我们。还没有。
发生这种情况是因为response.json()
和JSON.parse()
的类型来自 TypeScript 的标准库,其中这些方法具有显式的any
注释。我们仍然可以手动为变量指定更好的类型,但标准库中any
类型都出现了近 1,200 次。几乎不可能记住any
可以从标准库潜入我们的代码库的情况。
外部依赖也是如此。 npm 中有许多类型不佳的库,其中大多数仍然是用 JavaScript 编写的。因此,使用此类库很容易导致代码库中出现大量隐式any
。
一般来说,仍然有很多方法any
潜入我们的代码。
理想情况下,我们希望在 TypeScript 中有一个设置,使编译器抱怨任何因任何原因接收到any
类型的变量。不幸的是,目前不存在这样的设置,并且预计不会添加。
我们可以通过使用typescript-eslint
插件的类型检查模式来实现此行为。此模式与 TypeScript 结合使用,提供从 TypeScript 编译器到 ESLint 规则的完整类型信息。有了这些信息,就可以编写更复杂的 ESLint 规则,从本质上扩展 TypeScript 的类型检查功能。例如,规则可以查找具有any
类型的所有变量,无论any
是如何获得的。
要使用类型感知规则,您需要稍微调整 ESLint 配置:
module.exports = { extends: [ 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', + parserOptions: { + project: true, + tsconfigRootDir: __dirname, + }, root: true, };
要启用typescript-eslint
的类型推断,请将parserOptions
添加到 ESLint 配置中。然后,将recommended
预设替换为recommended-type-checked
。后一个预设添加了大约 17 条新的强大规则。出于本文的目的,我们将仅关注其中的 5 个。
no-unsafe-argument
规则搜索将any
类型的变量作为参数传递的函数调用。当这种情况发生时,类型检查就会丢失,并且强类型的所有好处也会丢失。
例如,让我们考虑一个需要对象作为参数的saveForm
函数。假设我们收到 JSON,解析它,并获得一个any
类型。
// ❌ Incorrect function saveForm(values: FormValues) { console.log(values); } const formValues = JSON.parse(userInput); // ^? any saveForm(formValues); // ^ Unsafe argument of type `any` assigned // to a parameter of type `FormValues`.
当我们使用此参数调用saveForm
函数时, no-unsafe-argument
规则将其标记为不安全,并要求我们为value
变量指定适当的类型。
该规则足够强大,可以深入检查函数参数内的嵌套数据结构。因此,您可以确信将对象作为函数参数传递将永远不会包含非类型化数据。
// ❌ Incorrect saveForm({ name: 'John', address: JSON.parse(addressJson), // ^ Unsafe assignment of an `any` value. });
修复错误的最佳方法是使用 TypeScript 的类型缩小或验证库,例如Zod或Superstruct 。例如,让我们编写parseFormValues
函数来缩小解析数据的精确类型。
// ✅ Correct function parseFormValues(data: unknown): FormValues { if ( typeof data === 'object' && data !== null && 'name' in data && typeof data['name'] === 'string' && 'address' in data && typeof data.address === 'string' ) { const { name, address } = data; return { name, address }; } throw new Error('Failed to parse form values'); } const formValues = parseFormValues(JSON.parse(userInput)); // ^? FormValues saveForm(formValues);
请注意,允许将any
类型作为参数传递给接受unknown
函数,因为这样做没有安全问题。
编写数据验证函数可能是一项繁琐的任务,尤其是在处理大量数据时。因此,值得考虑使用数据验证库。例如,对于 Zod,代码将如下所示:
// ✅ Correct import { z } from 'zod'; const schema = z.object({ name: z.string(), address: z.string(), }); const formValues = schema.parse(JSON.parse(userInput)); // ^? { name: string, address: string } saveForm(formValues);
no-unsafe-assignment
规则搜索值具有any
类型的变量赋值。此类赋值可能会误导编译器,使其认为变量具有某种类型,而数据实际上可能具有不同的类型。
考虑前面的 JSON 解析示例:
// ❌ Incorrect const formValues = JSON.parse(userInput); // ^ Unsafe assignment of an `any` value
由于no-unsafe-assignment
规则,我们甚至可以在将formValues
传递到其他地方之前捕获any
类型。修复策略保持不变:我们可以使用类型缩小来为变量的值提供特定类型。
// ✅ Correct const formValues = parseFormValues(JSON.parse(userInput)); // ^? FormValues
这两条规则触发的频率要低得多。但是,根据我的经验,当您尝试使用类型错误的第三方依赖项时,它们确实很有帮助。
如果变量具有any
类型,则no-unsafe-member-access
规则会阻止我们访问对象属性,因为它可能为null
或undefined
。
no-unsafe-call
规则阻止我们将any
类型的变量作为函数调用,因为它可能不是函数。
假设我们有一个类型错误的第三方库,名为untyped-auth
:
// ❌ Incorrect import { authenticate } from 'untyped-auth'; // ^? any const userInfo = authenticate(); // ^? any ^ Unsafe call of an `any` typed value. console.log(userInfo.name); // ^ Unsafe member access .name on an `any` value.
linter 突出了两个问题:
authenticate
函数可能不安全,因为我们可能会忘记将重要参数传递给该函数。userInfo
对象读取name
属性是不安全的,因为如果身份验证失败,该属性将为null
。
修复这些错误的最佳方法是考虑使用具有强类型 API 的库。但如果这不是一个选项,您可以自己扩充库类型。具有固定库类型的示例如下所示:
// ✅ Correct import { authenticate } from 'untyped-auth'; // ^? (login: string, password: string) => Promise<UserInfo | null> const userInfo = await authenticate('test', 'pwd'); // ^? UserInfo | null if (userInfo) { console.log(userInfo.name); }
no-unsafe-return
规则有助于避免意外地从应该返回更具体内容的函数中返回any
类型。这种情况可能会误导编译器认为返回值具有某种类型,而数据实际上可能具有不同的类型。
例如,假设我们有一个解析 JSON 并返回具有两个属性的对象的函数。
// ❌ Incorrect interface FormValues { name: string; address: string; } function parseForm(json: string): FormValues { return JSON.parse(json); // ^ Unsafe return of an `any` typed value. } const form = parseForm('null'); console.log(form.name); // ^ TypeError: Cannot read properties of null
parseForm
函数可能会在使用它的程序的任何部分导致运行时错误,因为未检查解析的值。 no-unsafe-return
规则可以防止此类运行时问题。
通过添加验证来确保解析的 JSON 与预期类型匹配,可以轻松解决此问题。这次我们使用 Zod 库:
// ✅ Correct import { z } from 'zod'; const schema = z.object({ name: z.string(), address: z.string(), }); function parseForm(json: string): FormValues { return schema.parse(JSON.parse(json)); }
使用类型检查规则会给 ESLint 带来性能损失,因为它必须调用 TypeScript 的编译器来推断所有类型。当在预提交挂钩和 CI 中运行 linter 时,这种速度下降主要是明显的,但在 IDE 中工作时并不明显。类型检查在 IDE 启动时执行一次,然后在更改代码时更新类型。
值得注意的是,仅推断类型比tsc
编译器的通常调用更快。例如,在我们最近的项目中,大约有 150 万行 TypeScript 代码,通过tsc
进行类型检查大约需要 11 分钟,而 ESLint 的类型感知规则引导所需的额外时间仅为大约 2 分钟。
对于我们的团队来说,使用类型感知静态分析规则所提供的额外安全性是值得权衡的。对于较小的项目,这个决定甚至更容易做出。
控制 TypeScript 项目中any
的使用对于实现最佳类型安全和代码质量至关重要。通过利用typescript-eslint
插件,开发人员可以识别并消除代码库中出现的any
类型,从而形成更健壮且可维护的代码库。
通过使用类型感知的 eslint 规则,我们的代码库中关键字any
的任何出现都将是经过深思熟虑的决定,而不是错误或疏忽。这种方法可以防止我们在自己的代码以及标准库和第三方依赖项中使用any
。
总体而言,类型感知 linter 使我们能够实现类似于 Java、Go、Rust 等静态类型编程语言的类型安全级别。这极大地简化了大型项目的开发和维护。
我希望您从本文中学到了新的东西。感谢您的阅读!