TypeScript中any的罪与罚:为什么要避免使用any?

19 阅读6分钟

在之前的文章中 TypeScript基础类型全解 ,我们了解到了 TypeScript 的所有基础类型,里面提到了 any 类型。那么 any 类型到底是什么呢?其实,any 就如潘多拉魔盒:用起来一时爽,维护时火葬场。本篇文章将通过各种案例,来演示 any 是如何悄无声息地破坏代码的类型安全,以及如何使用 unknown 优雅地解决问题。

any的"罪":类型系统的漏洞

any到底是什么?

any 是 TypeScript 中的一种特殊类型,它会告诉编译器:"别管这个值是什么类型,我全权负责"。即:any 会放弃所有类型检查。

let anything: any = "我是一个字符串";

anything = 42;            // ✅ 可以变成数字
anything = true;          // ✅ 可以变成布尔值
anything = { key: "value" }; // ✅ 可以变成对象
anything();               // ✅ 可以当作函数调用(尽管会崩溃)
anything.nonExistentProperty.anotherProperty; // ✅ 可以访问不存在的属性

any的类型传染

我们先来看一段简单的代码:

let age:number;
// age = '12'; // ❌ 不能将类型“string”分配给类型“number”
age = '12' as any; // ✅
age += 1;
console.log(age);

上述代码输出结果是多少呢?13吗?不对,真实结果是:'121' 。因为在类型声明中说明了 agenumber 类型,但使用了 any 断言分配给了一个 string 值,因此类型检查器仍然会相信它是一个 number,但代码运行时会以 string 处理。这就是 any 的类型传染问题,也是 any 没有类型安全的原因。

关于类型断言的话题,在后面的文章中会详细讲解。

any的"罚":类型灾难

any 会打破类型契约

当我们在编写一个函数时,其实我们是在指定一个契约:调用者需要传递特定类型的参数,函数会返回一个特定类型的输出。但 any 会打破这些类型契约:

function sum(a: number, b: number) {
  return a + b;
}
let a: any = '1';
let b: any = '2';
sum(a, b);

上述代码中,sum() 函数期望接收两个 number 类型的参数,而不是 string。any 类型打破了这种契约,这会很麻烦,因为 JavaScript 会在类型之间进行隐式转换,导致代码可以正常运行,但得不到正确的结果。

any会掩盖重构代码时的错误

当我们要对代码进行重构或者修改时,即使存在漏改,但编译器也不会报错:

// 初始版本:使用具体类型
interface User {
  id: number;
  name: string;
  age: number;
}

function greetUser(user: User): string {
  return `Hello, ${user.name}! You are ${user.age} years old.`;
}

现在,我们需要把 age 属性更改为 userAge 属性,在 greetUser() 函数传参时,使用 any 会有哪些问题呢?

interface User {
  id: number;
  name: string;
  userAge: number;
}
function greetUser(user: any): string {
  return `Hello, ${user.name}! You are ${user.age} years old.`;
}

其结果是:编译通过,但运行时报错!

any会屏蔽类型设计问题

对于一个复杂的类型定义可能会很长,相比写几十个属性类型而言,一个 any 确实能省很多事。但随之而来的,也会有很多问题,比如代码审查时,需要重新梳理和构建类型信息。我们无法从简单的代码层面来判断这个设计是好是坏,是否合理。

any会丧失语言服务

当一个符号有一个类型时,TypeScript的语言服务(Language Service)能够提供自动补全和上下文文档信息。但对于带有 any 类型的符号,会丧失这种功能,只能人为自己写了!

严格模式:给any戴上镣铐

针对 any 可能带来的种种问题,TypeScript 提供了严格模式选项,以此来限制 any 的使用:

noImplicitAny:禁止隐式any

// 在tsconfig.json中启用
{
  "compilerOptions": {
    "noImplicitAny": true
  }
}

// ❌ 错误:参数'value'隐式具有'any'类型
function process(value) {
  return value.toUpperCase();
}

// ✅ 正确:明确指定类型
function process(value: string) {
  return value.toUpperCase();
}

strict:最严格的检查

{
  "compilerOptions": {
    "strict": true  // 包含所有严格检查
  }
}

// strict包含:
// - noImplicitAny
// - strictNullChecks
// - strictFunctionTypes
// - strictBindCallApply
// - strictPropertyInitialization
// - noImplicitThis
// - alwaysStrict

@ts-expect-error:明确忽略错误

// 有时确实需要any,但应该明确标注

// ❌ 不好:直接使用any
function dangerousFunction(data: any) {
  // ...
}

// ✅ 更好:使用unknown,然后明确断言
function saferFunction(data: unknown) {
  if (typeof data === 'object' && data !== null) {
    const obj = data as { id: number };
    // 明确知道自己在做什么
  }
}

// ✅ 最好:使用@ts-expect-error注释
function trickyFunction() {
  // @ts-expect-error - 这里确实需要绕过类型检查
  const result: any = legacyLibrary.call();
  return result;
}

unknown:安全的any替代方案

unknown是什么?

unknown 是 TypeScript 3.0 引入的类型,它表示:"我不知道这是什么类型,所以你在用之前必须先检查"。

let uncertain: unknown = "Hello World";

// ❌ 不能直接使用
// uncertain.toUpperCase(); // 错误:'unknown'类型上不存在'toUpperCase'
// uncertain(); // 错误:不能调用
// uncertain.property; // 错误:不能访问属性

// ✅ 必须先进行类型检查
if (typeof uncertain === "string") {
  console.log(uncertain.toUpperCase()); // 现在可以了
}

unknown vs any:行为对比

特性anyunknown
赋值给其他类型✅ 总是可以❌ 需要类型断言
调用方法✅ 总是可以❌ 需要类型检查
访问属性✅ 总是可以❌ 需要类型检查
作为函数调用✅ 总是可以❌ 需要类型检查
类型安全❌ 不安全✅ 安全
用途放弃类型检查暂时未知类型

它们的关键区别在于:

  • any:我保证没问题(编译器相信你)
  • unknown:我不确定,你要先检查(编译器要求你证明)

何时可以使用any?

虽然我们要避免any,但在某些特定情况下,它还是有用的,比如:

  • 迁移JavaScript项目,在迁移期间,可以暂时使用any。
  • 测试代码,测试中可能需要模拟各种情况。
  • 与没有类型的第三方库交互。

使用any注意事项

我们在使用 any 时,要注意些什么呢?

限制any的作用范围

function processData(data: any) {
  // 立即转换为安全类型
  const safeData = data as { id: number; name: string };
  // 后续使用safeData,而不是data
  return safeData;
}

添加明确的注释

function riskyOperation(input: any /* 来自旧系统,无法修改 */) {
  // 业务逻辑
}

逐步替换

  1. 第一步:标记为any
  2. 第二步:添加类型断言
  3. 第三步:完全移除any
  4. 第四步:添加验证
// 第一步:标记为any
let value: any = getFromLegacySystem();

// 第二步:添加类型断言
let value: any = getFromLegacySystem();
const typedValue = value as string;

// 第三步:完全移除any
let value: string = getFromLegacySystem() as string;

// 第四步:添加验证
function safeGetString(): string {
  const value = getFromLegacySystem();
  if (typeof value !== "string") {
    throw new Error("Expected string");
  }
  return value;
}

结语

本文主要介绍了 any 和 unknown 两种类型的对比,以及 any 的使用危害等。在实际项目中还是推荐开启strict模式,并将所有 any 替换为 unknown,添加必要的类型检查。

对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!