? ?
? ?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 来充分利用 TypeScript。
Any 类型从何而来
了解any
代码库中类型的来源很重要,因为显式编写any
并不是唯一的选择。尽管我们尽最大努力避免使用该any
类型,但它有时会隐式潜入代码库。
any
代码库中该类型有四个主要来源:
- tsconfig.h 中的编译器选项
- TypeScript 的标准库。
- 项目依赖性。
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
,但不会阻止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 还不够
尽管我们已经处理了显式使用的,但项目的依赖项中any
仍然隐含着许多依赖项,包括 npm 包和 TypeScript 的标准库。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
变量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
规则,我们any
甚至可以在传递到formValues
其他地方之前捕获该类型。修复策略保持不变:我们可以使用类型缩小来为变量的值提供特定类型。
// ? Correct
const formValues = parseFormValues(JSON.parse(userInput));
// ^? FormValues
无不安全成员访问和无不安全调用
这两条规则触发的频率要低得多。但是,根据我的经验,当您尝试使用类型错误的第三方依赖项时,它们确实很有帮助。
no-unsafe-member-access
如果变量具有类型,则该规则会阻止我们访问对象属性any
,因为它可能是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
函数可能不安全,因为我们可能会忘记将重要参数传递给该函数。 name
从对象读取属性userInfo
是不安全的,如果身份验证失败也会如此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 等静态类型编程语言的类型安全级别。这极大地简化了大型项目的开发和维护。
我希望您从本文中学到了新的东西。感谢您的阅读!