TypeScript 声称是一种构建在 JavaScript 之上的强类型编程语言,可以在任何规模上提供更好的工具。然而, 包含 类型,它通常会隐式地潜入代码库并导致失去 TypeScript 的许多优点。 TypeScript any 本文探讨了控制 TypeScript 项目中的 类型的方法。准备好释放 TypeScript 的力量,实现最终的类型安全并提高代码质量。 any 在 TypeScript 中使用 Any 的缺点 TypeScript 提供了一系列附加工具来增强开发人员体验和生产力: 它有助于在开发阶段的早期发现错误。 它为代码编辑器和 IDE 提供了出色的自动完成功能。 它允许通过出色的代码导航工具和自动重构轻松重构大型代码库。 它通过类型提供附加语义和显式数据结构,简化了对代码库的理解。 但是,一旦您开始在代码库中使用 类型,您就会失去上面列出的所有好处。 类型是类型系统中的一个危险漏洞,使用它会禁用所有类型检查功能以及依赖于类型检查的所有工具。结果,TypeScript 的所有好处都消失了:错误被遗漏,代码编辑器变得不太有用等等。 any any 例如,考虑以下示例: 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 在第一种情况下,存在 错误,因为我们传递了数字而不是字符串。 TypeScript 无法突出显示错误,因为 会禁用类型检查。 TypeError: data.split is not a function any 在第二种情况下, 变量也具有 类型。这意味着 一次使用都会对代码库的大部分产生级联效应。 res2 any any 仅在极端情况或出于原型设计需要时才可以使用 。一般来说,最好避免使用 来充分利用 TypeScript。 any any Any 类型从何而来 了解代码库中 类型的来源非常重要,因为显式编写 并不是唯一的选择。尽管我们尽最大努力避免使用 类型,但它有时会隐式地潜入代码库。 any any any 代码库中的 类型有四个主要来源: any tsconfig.h 中的编译器选项 TypeScript 的标准库。 项目依赖性。 在代码库中显式使用 。 any 对于前两点,我已经写过关于 和 文章。如果您想提高项目中的类型安全性,请检查它们。 tsconfig 中的关键注意事项 改进标准库类型的 这次,我们将重点关注用于控制代码库中 类型的外观的自动工具。 any 第一阶段:使用 ESLint 是一种流行的静态分析工具,Web 开发人员使用它来确保最佳实践和代码格式化。它可用于强制编码风格并查找不遵守某些准则的代码。 ESLint 由于 插件,ESLint 还可以与 TypeScript 项目一起使用。最有可能的是,这个插件已经安装在您的项目中。但如果没有,您可以按照官方 进行操作。 typesctipt-eslint 入门指南 最常见的配置如下: typescript-eslint module.exports = { extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', root: true, }; 此配置使 能够在语法级别理解 TypeScript,从而允许您编写适用于代码中手动编写的类型的简单 eslint 规则。例如,您可以禁止显式使用 . eslint any 预设包含一组精心挑选的 ESLint 规则,旨在提高代码正确性。虽然建议使用整个预设,但出于本文的目的,我们将仅关注 规则。 recommended 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 类型推断 为什么 no-explicit-any 还不够 尽管我们已经处理了显式使用的 ,但项目的依赖项中仍然存在许多隐含的 ,包括 npm 包和 TypeScript 的标准库。 any any 考虑以下代码,它可能在任何项目中看到: 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 变量 和 都隐式指定为 类型。在这种情况下 和 TypeScript 的严格模式都不会警告我们。还没有。 pokemons settings any no-explicit-any 发生这种情况是因为 和 的类型来自 TypeScript 的标准库,其中这些方法具有显式的 注释。我们仍然可以手动为变量指定更好的类型,但标准库中 类型都出现了近 1,200 次。几乎不可能记住 可以从标准库潜入我们的代码库的情况。 response.json() JSON.parse() any any any 外部依赖也是如此。 npm 中有许多类型不佳的库,其中大多数仍然是用 JavaScript 编写的。因此,使用此类库很容易导致代码库中出现大量隐式 。 any 一般来说,仍然有很多方法 潜入我们的代码。 any 第二阶段:增强类型检查能力 理想情况下,我们希望在 TypeScript 中有一个设置,使编译器抱怨任何因任何原因接收到 类型的变量。不幸的是,目前不存在这样的设置,并且预计不会添加。 any 我们可以通过使用 插件的类型检查模式来实现此行为。此模式与 TypeScript 结合使用,提供从 TypeScript 编译器到 ESLint 规则的完整类型信息。有了这些信息,就可以编写更复杂的 ESLint 规则,从本质上扩展 TypeScript 的类型检查功能。例如,规则可以查找具有 类型的所有变量,无论 是如何获得的。 typescript-eslint 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, }; 要启用 的类型推断,请将 添加到 ESLint 配置中。然后,将 预设替换为 。后一个预设添加了大约 17 条新的强大规则。出于本文的目的,我们将仅关注其中的 5 个。 typescript-eslint parserOptions recommended recommended-type-checked 无不安全论点 规则搜索将 类型的变量作为参数传递的函数调用。当这种情况发生时,类型检查就会丢失,并且强类型的所有好处也会丢失。 no-unsafe-argument any 例如,让我们考虑一个需要对象作为参数的 函数。假设我们收到 JSON,解析它,并获得一个 类型。 saveForm 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 启动时执行一次,然后在更改代码时更新类型。 值得注意的是,仅推断类型比 编译器的通常调用更快。例如,在我们最近的项目中,大约有 150 万行 TypeScript 代码,通过 进行类型检查大约需要 11 分钟,而 ESLint 的类型感知规则引导所需的额外时间仅为大约 2 分钟。 tsc tsc 对于我们的团队来说,使用类型感知静态分析规则所提供的额外安全性是值得权衡的。对于较小的项目,这个决定甚至更容易做出。 结论 控制 TypeScript 项目中 的使用对于实现最佳类型安全和代码质量至关重要。通过利用 插件,开发人员可以识别并消除代码库中出现的 类型,从而形成更健壮且可维护的代码库。 any typescript-eslint any 通过使用类型感知的 eslint 规则,我们的代码库中关键字 的任何出现都将是经过深思熟虑的决定,而不是错误或疏忽。这种方法可以防止我们在自己的代码以及标准库和第三方依赖项中使用 。 any any 总体而言,类型感知 linter 使我们能够实现类似于 Java、Go、Rust 等静态类型编程语言的类型安全级别。这极大地简化了大型项目的开发和维护。 我希望您从本文中学到了新的东西。感谢您的阅读! 有用的链接 库:typescript-eslint 文章:tsconfig 中的关键注意事项 文章:改进标准库类型 文档:TypeScript 中的类型缩小 文档:TypeScript 中的类型推断 图书馆:佐德 图书馆:上层建筑