在之前的文章中 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' 。因为在类型声明中说明了 age 是 number 类型,但使用了 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:行为对比
| 特性 | any | unknown |
|---|---|---|
| 赋值给其他类型 | ✅ 总是可以 | ❌ 需要类型断言 |
| 调用方法 | ✅ 总是可以 | ❌ 需要类型检查 |
| 访问属性 | ✅ 总是可以 | ❌ 需要类型检查 |
| 作为函数调用 | ✅ 总是可以 | ❌ 需要类型检查 |
| 类型安全 | ❌ 不安全 | ✅ 安全 |
| 用途 | 放弃类型检查 | 暂时未知类型 |
它们的关键区别在于:
- 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 /* 来自旧系统,无法修改 */) {
// 业务逻辑
}
逐步替换
- 第一步:标记为any
- 第二步:添加类型断言
- 第三步:完全移除any
- 第四步:添加验证
// 第一步:标记为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,添加必要的类型检查。
对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!