第 5 条:限制 any 类型的使用
要点
- TypeScript 的
any
类型会关闭大部分类型检查,让变量变得“随意”。 - 使用
any
会导致:失去类型安全、破坏参数约定、影响开发体验、增加重构风险、隐藏类型设计,并削弱对 TypeScript 的信任。 - 尽量避免使用
any
!
正文
TypeScript 的类型系统是渐进式和可选的:渐进式是因为你可以逐步为代码添加类型(通过 noImplicitAny
),可选是因为你可以随时禁用类型检查器。这些特性的关键是 any
类型:
let ageInYears: number
ageInYears = '12'
// ~~~~~~~ Type 'string' is not assignable to type 'number'.
ageInYears = '12' as any // OK
类型检查器在这里提示是正确的,但你可以通过将其类型指定为 any
来消除这个警告。
在开始使用 TypeScript 时,当你不理解某个错误、认为类型检查器的判断有误,或者只是懒得写类型声明时,很容易会使用 any
类型和类型断言(as any
)。
在某些情况下,这样做是可以接受的,但要注意,使用 any
会丧失许多 TypeScript 带来的优势。在使用之前,你至少应当了解它的风险。
any
类型没有类型安全
在前面的例子中,类型声明表明 ageInYears
是一个数字。但 any
允许你将一个字符串赋值给它。类型检查器会认为它是一个数字(毕竟你声明了它是数字),而这种混乱将无法被捕获:
ageInYears += 1 // OK; at runtime, ageInYears is now "121"
any
类型非常宽松
当你写一个函数时,你是在设定规则:如果传入某种类型的输入,就会返回预期的输出。但使用 any
类型后,你就能绕过这些规则,随意传入任何类型的值。
function calculateAge(birthDate: Date): number {
// ...
}
let birthDate: any = '1990-01-19'
calculateAge(birthDate) // OK
birthDate
参数应该是一个 Date
类型,而不是一个字符串。any
类型让你打破了 calculateAge
的规则。这尤其有问题,因为 JavaScript 常常会隐式地在类型之间进行转换。有时字符串可以替代数字正常工作,但在其他情况下却会出错。
any
类型没有智能提示
当一个变量的类型不是 any
时,TypeScript 可以提供智能补全和上下文文档提示。
但如果变量的类型是 any
,那就没有任何提示了。
重命名也是类似的功能之一。如果你有一个 Person
类型,以及用于格式化人名的函数:
interface Person {
first: string
last: string
}
const formatName = (p: Person) => `${p.first} ${p.last}`
const formatNameAny = (p: any) => `${p.first} ${p.last}`
然后,你可以在编辑器中选中 first
,选择“重命名符号”,将其改为 firstName
。
这样会修改 formatName
函数中的 first
,但不会影响 any
类型的那一版:
interface Person {
firstName: string
last: string
}
const formatName = (p: Person) => `${p.firstName} ${p.last}`
const formatNameAny = (p: any) => `${p.first} ${p.last}`
TypeScript 的标语之一是“能扩展的 JavaScript”。其中“扩展”的关键之一就是语言服务,它是 TypeScript 体验的重要组成部分(详见第 6 条)。如果失去了这些服务,不仅会降低你的开发效率,也会影响与你协作的其他人。
any
类型会在重构时掩盖 bug
假设你正在开发一个 Web 应用,用户可以选择某个项目。其中一个组件可能会有一个 onSelectItem
回调函数。给 item
定义类型感觉有点麻烦,所以你暂时用 any
代替:
interface ComponentProps {
onSelectItem: (item: any) => void
}
下面是调用该组件的代码:
function renderSelector(props: ComponentProps) {
/* ... */
}
let selectedId: number = 0
function handleSelectItem(item: any) {
selectedId = item.id
}
renderSelector({ onSelectItem: handleSelectItem })
后来,你对选择器进行了改动,使得直接把整个 item
对象传递给 onSelectItem
变得不太方便。但这没关系,因为你只需要它的 ID。所以,你修改了 ComponentProps
中的函数参数类型:
interface ComponentProps {
onSelectItem: (id: number) => void
}
你更新了组件,类型检查器也没报错,大功告成!
……但真的吗?handleSelectItem
的参数是 any
,所以无论传进来的是 item
还是 ID
,它都不会报错。结果,代码在运行时抛出了异常,而类型检查器却毫无察觉。
如果你当初使用了更具体的类型,这个问题就能更早被类型检查器发现,从而避免问题产生。
any
让你的类型设计变得隐晦
在应用程序中,某些对象(比如应用状态)可能会非常复杂,包含几十个属性。你可能觉得给这些属性一一定义类型太麻烦,干脆用 any
省事。
这样做的问题不仅仅是前面提到的那些,还会让你的状态设计变得隐晦。第四章会讲到,良好的类型设计对编写清晰、正确、易懂的代码至关重要。而 any
让你的类型设计变得隐晦,导致代码可读性下降,甚至连你自己都难以判断这个设计是否合理。如果你的同事帮你审核代码,他们需要先搞清楚这个 any
到底代表什么,而不是直接看到清晰的类型定义。写明类型,能让大家一目了然。
any
会削弱你对类型系统的信任
每次类型检查器帮你发现一个错误,你都会更信赖 TypeScript。但如果你在运行时碰到类型错误,而 TypeScript 之前毫无察觉,你对它的信任度就会下降。
如果你在团队里推广 TypeScript,而代码里到处都是 any
,你的同事可能会怀疑 TypeScript 真的值得投入精力吗?许多这种“漏掉的错误”都是 any
造成的。
TypeScript 本来是为了让开发更轻松,但如果代码里 any
太多,可能会比纯 JavaScript 还难用。因为你不仅要处理类型错误,还要自己记住变量的真实类型。理想情况下,你的类型应该尽可能准确,这样 TypeScript 就能帮你记住这些信息,而不是让你自己去猜。
当然,有时候 any
还是不可避免的。不过,使用 any
也有更好的方式来降低它的负面影响,具体方法可以参考第五章。
关键点总结
- TypeScript 的
any
类型会关闭大部分类型检查,让变量变得“随意”。 - 使用
any
会导致:失去类型安全、破坏参数约定、影响开发体验、增加重构风险、隐藏类型设计,并削弱对 TypeScript 的信任。 - 尽量避免使用
any
!