paint-brush
让 TypeScript 真正成为“强类型”by@nodge
15,793
15,793

让 TypeScript 真正成为“强类型”

Maksim Zemskov12m2023/09/10
Read on Terminal Reader

TypeScript 为预先未知数据形状的情况提供“Any”类型。但是,过度使用此类型可能会导致类型安全、代码质量和开发人员体验方面的问题。本文探讨了与“任何”类型相关的风险,确定了其包含在代码库中的潜在来源,并提供了在整个项目中控制其使用的策略。
featured image - 让 TypeScript 真正成为“强类型”
Maksim Zemskov HackerNoon profile picture
0-item
1-item

TypeScript 声称是一种构建在 JavaScript 之上的强类型编程语言,可以在任何规模上提供更好的工具。然而, TypeScript包含any类型,它通常会隐式地潜入代码库并导致失去 TypeScript 的许多优点。


本文探讨了控制 TypeScript 项目中的any类型的方法。准备好释放 TypeScript 的力量,实现最终的类型安全并提高代码质量。

在 TypeScript 中使用 Any 的缺点

TypeScript 提供了一系列附加工具来增强开发人员体验和生产力:


  • 它有助于在开发阶段的早期发现错误。
  • 它为代码编辑器和 IDE 提供了出色的自动完成功能。
  • 它允许通过出色的代码导航工具和自动重构轻松重构大型代码库。
  • 它通过类型提供附加语义和显式数据结构,简化了对代码库的理解。


但是,一旦您开始在代码库中使用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类型有四个主要来源:

  1. tsconfig.h 中的编译器选项
  2. TypeScript 的标准库。
  3. 项目依赖性。
  4. 在代码库中显式使用any


对于前两点,我已经写过关于tsconfig 中的关键注意事项改进标准库类型的文章。如果您想提高项目中的类型安全性,请检查它们。


这次,我们将重点关注用于控制代码库中any类型的外观的自动工具。

第一阶段:使用 ESLint

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 ,但它不会阻止显式使用anyno-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也会由于类型推断而对代码库的很大一部分产生级联影响。然而,这距离实现最终的类型安全还很远。

为什么 no-explicit-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


变量pokemonssettings都隐式指定为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 的类型缩小或验证库,例如ZodSuperstruct 。例如,让我们编写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规则会阻止我们访问对象属性,因为它可能为nullundefined


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 等静态类型编程语言的类型安全级别。这极大地简化了大型项目的开发和维护。


我希望您从本文中学到了新的东西。感谢您的阅读!

有用的链接