开始看本文之前,我希望读者朋友们知道我在很大程度上是一位 TypeScript 粉丝。在我的前端 React 项目和各种后端 Node 工作里,所使用的主要编程语言都是 TypeScript。我是这条船上的人,但也确实有一些疑惑,想在这篇文章中讨论一下。到目前为止,我已经使用 TypeScript 写了至少三年的代码,涉及的项目不计其数,因此可以说 TypeScript 的确是走在了正路上,或者说满足了某种需求。
TypeScript 克服了一些难以逾越的障碍,成为了前端编程领域的主流之选。TypeScript 在这篇文章(https://www.agiratech.com/most-in-demand-programming-languages-learn-2020/)列出的最受欢迎编程语言中排名第七。
无论你是否在使用 TypeScript,不管多大规模的软件团队都应遵循以下实践:
-
精心编写的单元测试应在合理范围内覆盖尽可能多的生产代码;
-
结对编程——另一双眼睛可以捕捉到的远不止语法错误;
-
良好的同行评审流程——正确的同行评审可以找出许多机器无法捕获的错误;
-
使用 eslint 之类的 linter。
TypeScript 可以在这些基础之上增加额外的一层安全性,但我认为这在编程语言的需求列表中是排在非常靠后的位置上的。
我认为这可能是 TypeScript 当前版本面临的主要问题,但首先,我来定义一下什么是健全(sound)和不健全(unsound)的类型系统。
健全
一个健全的类型系统能确保你的程序不会进入无效状态。例如,如果一个表达式的静态类型是 string,则在运行时,对它求值时你肯定只会得到一个 string。
在健全的类型系统中,永远不会在编译时 或运行时 出现表达式与预期类型不匹配的情况。
// 类型 'string' 不能赋值给 类型 'number'
const increment = (i: number): number => { return i + "1"; }
// 类型 '"98765432"' 的参数不能赋值给类型 'number' 的参数.
const countdown: number = increment("98765432");
关于 Typescript,以下事实是非常明确的:100%的健全性不是它的目标,并且 TypeScript 的非目标列表(https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals)中的 3 号非目标明确指出:
应用一个健全或“正确无误”的类型系统(不是我们的目标)。相反,要在正确性和生产力之间取得平衡。
interface A {
x: number;
}
let a: A = {x: 3}
let b: {x: number | string} = a;
b.x = "unsound";
let x: number = a.x; // 不健全
a.x.toFixed(0); // 这啥?
a.x.toFixed(0);
我认为这可能是 TypeScript 的最大问题,那就是健全性不是它的目标。我仍会遇到许多运行时错误,这些错误没有被 tsc 编译器标记出来,但如果 TypeScript 有一个健全的系统就能做到了。采用这种方法,意味着 TypeScript 在健全和不健全的阵营之间是在脚踩两只船。这种摇摆路线遇上 any 类型就更严重了,我将在后文提到这一点。我还是得编写尽可能多的测试,这让我感到沮丧不已。当我第一次开始使用 TypeScript 时,我错误地得出了一个结论,以为以后就用不着编写这么多单元测试了。
TypeScript 挑战了现状,声称降低用户使用类型的认知开销比类型健全更重要。
我理解为什么 TypesScript 会走这条路,并且有一个观点指出,如果类型系统的健全性得到 100%保证,那么 TypeScript 的采用率就不会那么高了。随着 dart 语言逐渐流行(因为 Flutter 现已广泛应用),这个看法也被证伪了。健全性是 dart 语言的一个目标,可以参考这里的讨论:
https://dart.dev/guides/language/sound-dart
TypeScript 的不健全性,以及它在严格的类型系统上开的一大堆天窗拖累了它的效率,让它在今天处于相当 鸡肋 的位置上,非常可惜。我的愿望是,随着 TypeScript 的流行,会有更多的编译器选项可供使用,从而使高级用户可以争取 100%的健全性。
运行时类型检查不是 TypeScript 的目标之一,因此这种愿望可能永远不会实现。例如,在处理从 API 调用返回的 JSON 负载时,运行时类型检查会很有用。如果我们可以在类型级别上控制它,就用不着一整筐错误和许多单元测试了。
const getFullName = async (): string => {
const person: AxiosResponse = await api();
//response.name.fullName 可能在运行时成为 undefined
return response.name.fullName
}
有一些支持这种需求的库,例如 io-ts;虽然它很棒,但可能意味着你必须复制你的模型。
any 类型就是字面意思,编译器允许任何操作或赋值。
export class Person {
public _id: any;
public name: any;
public icon: any;
("oh my goodness" as any).ToFixed(1); // 记得我提到健全性时说的话吗?
strict 编译器选项会启用以下编译器设置,这些设置会让事物看起来更健全:
-
--strictNullChecks
-
--noImplicitAny
-
--noImplicitThis
-
--alwaysStrict
还有 eslint 规则 @typescript-eslint/no-explicit-any:
https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-explicit-any.md
any 的扩散会毁掉你类型系统的健全性。
我必须重申一点,我是 TypeScript 粉丝,也在日常工作中使用它,但我确实认为它有很多缺陷,而且炒作有些过头了。Airbnb 声称 TypeScript 可以阻止 38%的错误。我非常怀疑这一精确的百分比数字。TypeScript 不会给已有的良好实践锦上添花。我还是得编写尽可能多的测试。你可能不相信,我编写的代码变多了,可能还得编写许多类型测试。我仍然会遇到意外的运行时错误。
TypeScript 提供了比较基础的类型检查,但健全性和运行时类型检查不是它的目标,这让 TypeScript 处于脚踩两条船的状态,一只脚踏上了更好的那艘,一只脚还呆在现在的破船上,真是不幸。
TypeScript 的亮点在于良好的 IDE 支持,比如说 vscode 里如果我们打错什么内容,就会获得视觉反馈。
vscode 中的 TypeScript 错误
使用 TypeScript 还可以增强重构能力,并且在对修改后的代码运行 TypeScript 编译器时,可以立即识别出代码中断(例如方法签名的更改)。
TypeScript 带来了良好的类型检查,并且绝对比没有类型检查器或仅使用普通的 eslint 要更好,但是我认为它能更进一步。另外对于我们这种想要更多能力的用户来说,它应该可以提供足够多的编译器选项才是。
延伸阅读
https://blog.logrocket.com/is-typescript-worth-it/
最近几年,很多新的语言凭借更现代的语言特性、更高的开发效率,以及背后大厂的支持,应用越来越广泛。
GMTC 全球大前端技术大会(北京站)设置“编程语言”专题。本专题将关注新语言(比如:TypeScript、Swift、Kotlin 和 Dart 等)在具体业务开发中的选型思考,以及替代遗留项目时的典型解决方案。
疫情期间,大会也会为大家送上 3 月福利,大会门票限时 5 折 起,还有极 客时间企业版年卡 免费送!活动截止 3 月 31 日,已进入倒计时,还没有参与的你抓紧喽!
联系票务经理鱼丸:13269078023(同微信)
可以扫描下图二维码或者点击 【阅读原文】,查看官网了解更多详情!
文章评论