← 返回文章列表
查看 ↗

TypeScript作为JavaScript的超集:为什么它成为了现代开发的首选

cover

TypeScript作为JavaScript的超集

那是一个深夜,办公室的灯管嗡嗡作响,空气中漂浮着咖啡冷却后的苦涩气息。我的同事老周坐在我对面,屏幕的蓝光把他的脸切割成明暗两半。他的手指悬在键盘上方,已经保持了五分钟这个姿势。

“找出来了吗?”我问。

“类型错误。”他头也不抬,“第2847行和第3012行的返回类型对不上。整个系统跑起来没问题,但编译阶段就已经埋了一颗雷。”

我不知道怎么形容那个瞬间——他的屏幕上铺满了密密麻麻的JavaScript代码,那些括号、引号、箭头函数,像一片没有路标的原始森林。变量在函数之间穿梭,有时是字符串,有时是数字,有时又什么都不是。而老周就在这片森林里,凭着经验和直觉,一点一点寻找那颗埋在土里的雷。

那是我第一次认真想,如果这些变量从一开始就有名字,如果每一道穿梭都有迹可循,事情会不会不一样。

很多年后,我知道了TypeScript。


如果要找一个词来描述TypeScript,我会说它是JavaScript的骨骼。

这不是一个精确的比喻,却是我能想到的最诚实的一个。JavaScript是一门极其灵活的语言,像水一样善于流动、善于变形——同一个变量,可以在这一秒是一个数字,下一秒就变成一个对象,再下一秒又成了一个函数。它没有形状的执念,不给任何东西命名,不要求任何承诺。这是它的自由,也是它的代价。

当代码量从几十行蔓延到几万行,当团队从一个人扩大到几十个人,当交付周期从一周压缩到一天,这种自由就开始显出它的另一面。没有形状的东西,摔碎了就什么都不是。

而TypeScript在JavaScript的肉体里,植入了骨骼。

骨骼不说漂亮话,骨骼只是框架。它规定:这里必须是一个数字,那里必须是一个字符串,这个函数的输入是什么,输出又应该是什么。骨骼不限制肉体的生长方向,但骨骼让所有的生长都有迹可循。

这是TypeScript最根本的价值:不是消灭JavaScript,而是给JavaScript一个可以站立的样子。

JavaScript vs TypeScript


2012年,微软发布了TypeScript。

那时候JavaScript已经是Web世界的事实标准,但它的问题也在大规模应用中暴露得越来越清晰。一个前端项目如果超过一万行代码,维护成本就开始指数级攀升——不是因为业务逻辑复杂,而是因为那些看不见的变量。它们从哪里来,往哪里去,在什么情况下会变成什么。

TypeScript的回应是类型系统。

类型系统不是凭空发明的东西。在C语言里,在Java里,在Go语言里,类型无处不在。它们是程序员的语言里最接近法律条文的东西——规定了什么是允许的,什么是禁止的,违反了会怎样。TypeScript把这些带进了JavaScript的世界。

最直观的体现是类型注解。你可以给变量加上一行标注:let count: number = 0;。这行代码在运行时什么都不做,它不改变count的值,不改变程序的行为。它只是说:这个变量,从今往后是数字。如果你试图把它赋值为一个字符串,TypeScript的编译器会在你按下保存键的那一刻告诉你——错误。这不是运行时的错误,这是编译时的错误。错误发生在代码还在写字间里的时候,而不是已经部署到服务器上、在凌晨三点把生产环境的用户页面弄成一片空白的时候。

TypeScript把这个能力叫作编译时的“安全网”。

我想起老周在那个深夜的办公室里,趴在屏幕前数第2847行和第3012行。如果他用的是TypeScript,那个不一致在编译阶段就会浮出水面。也许屏幕上会亮起一行红色的下划线,旁边安静地写着:Type 'string' is not assignable to type 'number'。 不需要半夜三更,不需要咖啡,不需要在原始森林里凭直觉摸索。骨骼会自己说出问题在哪里。

当然,TypeScript不是僵硬的。它有自己的智慧,这种智慧叫作类型推断。编译器不是只会照本宣科的文书,它会阅读代码,理解上下文,然后自动推导出一个变量的类型。你不需要为每一个变量都加上注解——如果一个变量在声明时被赋值为数字,TypeScript会自动记住它是一个数字。除非你主动改变它。

这种克制是TypeScript迷人的地方。它不要求你把一切都写出来,它只是在你需要的时候,提供一个框架。


骨骼之上,还要有器官。TypeScript在这个意义上引入了接口。

接口是什么?我的理解是:接口是一份契约。

当你定义一个接口——比如一个用户的结构——你是在说:一个用户,必须有id,必须有name,email是可选的,而createdAt是只读的。这些规则被写进了契约里,之后任何试图违反契约的代码都会在编译阶段被拦截。

这在大型项目里意味着什么?意味着当你拿到一个API响应的时候,你不需要猜测它的结构。接口就在那里:用户数据应该长什么样,分页结果应该长什么样,错误信息应该长什么样。你不需要去读文档,不需要去打印console.log然后猜,类型系统已经把答案写进了代码本身。

有一句话说,最好的文档是代码本身。TypeScript让这句话变得可以执行。

我常常觉得接口是TypeScript里最被低估的特性。人们谈论泛型,谈论条件类型,谈论那些看起来很炫酷的高级特性。但接口才是每天都在用的东西——每一次你定义一个组件的props,每一次你声明一个函数的参数类型,你都在使用接口。它是骨骼上面的肌肉,是最日常、最沉默、也最可靠的部分。

TypeScript 类型系统架构


如果说类型系统和接口是TypeScript的静态之美,那工具链就是它的动态之美。

工具链这个词听起来很工业、很硬,但我更愿意把它想象成神经——连接大脑和身体的那个网络,让骨骼知道肉体在哪里,让肉体知道骨骼在做什么。

TypeScript的编译器tsc就是这样一根神经。它读取你的代码,检查类型,输出纯净的JavaScript。整个过程干净利落,像一个无声的翻译官,把你写的带有骨骼的代码,翻译成任何JavaScript运行环境都能理解的语言。

但这只是第一步。真正让TypeScript发光的是IDE的支持。

当你用VS Code打开一个TypeScript文件,智能提示就开始工作了。你在写一个函数,函数需要一个参数,参数是一个复杂的对象——VS Code会根据接口的定义,给你列出这个对象里每一个属性的名字和类型。你在调用一个方法,光标移过去,VS Code会告诉你这个方法期待什么,返回什么,可能抛出什么错误。这不是魔法,这是骨骼在说话。骨骼一直都在那里,它只是在通过IDE告诉你:我在这里,我是什么形状。

这种体验是JavaScript无法提供的。在JavaScript里,IDE只能猜测,它根据你写过的代码推断你接下来想写什么——猜对了是运气,猜错了是常态。而TypeScript不需要猜。类型就是类型,形状就是形状,IDE只需要照着读。

一个做React开发的朋友告诉我,他第一次在TypeScript项目里用VS Code写组件props的时候,有种被什么东西轻轻托住的感觉。“之前写props的时候总是要开两个窗口,一个写代码,一个查文档。现在IDE直接告诉我每个prop叫什么、是什么类型、是不是可选的。感觉像有人在旁边递工具。”

这种被托住的感觉,也许就是TypeScript工具链最温柔的那一面。


但TypeScript最让我着迷的,不是它解决了什么问题,而是它提出的问题本身。

当一门语言开始关心类型,它实际上在问一个更根本的问题:代码的确定性,到底值多少?

JavaScript选择了动态的确定性:代码跑起来之前,我不确定任何事情。如果它能跑,它就是对的。这是极致的实用主义,也是一种危险的乐观。

TypeScript选择了另一种确定性:在我跑这段代码之前,我要知道它是什么形状。类型错误是错误,运行时错误也是错误——但前者是可以避免的,后者是需要付出代价的。

微软内部做过一个研究,样本是超过五万行代码的大型项目。研究发现,TypeScript的类型系统能减少约30%的运行时错误。30%。这意味着每十次线上故障里,有三次可以被提前拦截在编译阶段。这不是小数,这是真实的质量,是真实的生产力,是真实的世界里那些本来应该在凌晨三点接到电话的工程师,可以安稳睡到天亮。

当然,TypeScript不是银弹。它有成本。类型注解需要写,类型错误需要修,渐进式迁移需要时间和耐心。对于小项目来说,这些成本可能超过收益。但当代码量突破某个临界点,当团队规模超过某个阈值,TypeScript的成本曲线就开始向下倾斜——它投入的每一分钟,都会在未来的维护中节省出更多的分钟。

一个在蚂蚁金服工作的朋友告诉我,他们把Ant Design从JavaScript迁移到TypeScript的时候,采取的是分模块逐步迁移的策略。“一开始很慢,每天只能迁移几个组件。但是因为TypeScript允许和JavaScript混用,我们没有停下来的压力。”他说,“后来快起来了,因为前端工程师慢慢找到了感觉。感觉对了之后,类型注解写起来很快。”最终,整个迁移完成了,成本比他们预期的低得多。

这大概是TypeScript最诚实的地方:它不要求你一下子改变一切。它允许你从.javaScript一步一步走到.ts,一点一点给代码长出骨骼。这种渐进式的温柔,让改变成为可能。


时间来到2024年,TypeScript已经成为GitHub上最广泛使用的编程语言之一。这个位置不是靠营销得来的,是靠无数开发者在无数个深夜里,一次一次选择的结果。

而在这个时间节点上,TypeScript又迎来了一个新的故事线:AI编程工具。

这个故事要从Claude Code说起。

Claude Code是Anthropic推出的AI编程工具,作为VS Code的官方扩展存在。它的核心功能是智能补全、代码解释、错误诊断和重构建议——听起来和其他AI编程助手没有本质区别,但它的开发团队做出了一个耐人寻味的选择:TypeScript。

为什么是TypeScript?

我们先退后一步,看一看AI编程工具面临的技术挑战。传统软件的问题是可以预见的——函数调用错误、空指针、类型不匹配,这些是有限的、已知的错误模式。但AI编程工具要处理的是另一种不确定性:AI模型生成的代码本身是不可预测的。同一个需求,模型可能生成十几种不同的实现,每种实现都语法正确,但逻辑可能南辕北辙。模型可能返回一个字符串,其中包含了代码块;也可能返回一个JSON,其中嵌入了更复杂的嵌套结构。这种不确定性,是传统静态类型系统最不擅长处理的领域。

TypeScript的介入,提供了一个有趣的可能。

关键在于泛型和条件类型。泛型允许你定义不拘泥于具体类型的函数和接口——你可以说:这是一个工具,它的输入是T,输出是U,至于是T是什么、U是什么,我们稍后再定。这对于需要处理多种AI工具的Claude Code来说是理想的设计模式:每一种AI工具都有自己的输入输出格式,但它们可以被统一在一个泛型接口之下。编译器会确保每一种工具的具体实现都符合这个接口的约束——也就是说,当你在代码里调用任何一个AI工具的时候,TypeScript会保证你传入的参数是正确类型的,你接收的返回值是可以预期结构的。

这不是运行时检查,这是编译时的铁律。铁律意味着,如果一个AI工具的输入输出类型不匹配,代码根本不会被允许运行。在一个人类程序员和AI模型共同协作的环境里,这种铁律的价值是双重的:它不仅约束了人类程序员的代码,也约束了AI生成代码的边界。

条件的类型,则是另一把利器。条件类型允许你写这样的代码:如果T是string,那么返回number,否则返回boolean。它让类型系统本身拥有了逻辑判断的能力。对于需要处理多变AI响应的Claude Code来说,条件类型让它能够在编译时推导出不同分支下的具体类型——即使AI的输出在编译时是未知的,TypeScript的条件类型依然能够追踪每一种可能的路径。

一个具体的例子:Claude Code需要处理超过四十种不同的AI工具,每种工具都有不同的参数和返回值结构。如果用JavaScript来写,所有工具的输入输出都会被当作any类型——编译器彻底放弃检查,一切依赖程序员的记忆和文档。一旦某个工具的API发生变化,依赖这个工具的代码不会收到任何警告,只有在运行时才会以某种奇怪的错误形式暴露出来。而TypeScript的条件类型配合工具类型(比如Partial、Pick、Omit),让Claude Code能够在编译时就建立起每一种工具的精确描述。一旦API变化,TypeScript的编译器会立刻指出哪些调用点需要更新。

这种能力,我愿意把它叫作编译时的对话。代码在编译阶段就开始说话了——说什么?说哪里不对了,说哪里可能出问题了,说这个数据结构的形状是什么。这是一种寂静的、提前的预警机制。

AI与TypeScript协作


更有意思的是,TypeScript的类型系统不仅在约束AI生成的代码,它也在和AI模型形成一种协同。

研究表明,TypeScript的类型系统能减少约30%的运行时错误。这个数字和AI生成代码中常见的错误类型高度重叠。AI模型在生成代码时,最常见的错误不是逻辑错误——逻辑错误有时反而不容易被察觉——而是类型错误:返回值的类型不对,参数的位置填反了,某个变量的类型被假设为某种它不是的东西。这些错误在TypeScript面前无所遁形。编译器一眼就能看到,然后标记出来。

这带来了一种奇特的画面:在AI编程工具的语境下,TypeScript的类型系统扮演了一个安静的质量守门员的角色。AI生成的代码在汇入项目之前,必须先通过TypeScript的编译检查。检查通过,代码进入;检查不通过,错误被拦截。一个在JavaScript环境下需要人工审查半小时才能发现的类型错误,在TypeScript的加持下,可能在保存文件的零点几秒内就被指出来了。

这不是取代人的判断,而是放大人的能力。TypeScript承担了那些机械的、重复的、最容易出错的类型检查工作,让人类工程师可以把精力放在真正需要创造力和判断力的事情上——架构设计、逻辑实现、边界条件的处理。

Claude Code的开发团队对此有过一个很精炼的表述:TypeScript是他们工具链的信任基础。当AI模型生成了一段代码,他们需要某种方式确认这段代码在类型层面是可靠的。TypeScript提供了这种方式,而且是自动的、实时的、零成本的。


VS Code本身是用TypeScript开发的。这件事我觉得值得单独说一说。

一个工具,如果它自己不用它所推荐的东西,它对这种东西的理解就是可疑的。但VS Code不是。VS Code从头到尾都是用TypeScript写成的,它的扩展API也使用TypeScript定义。这意味着,当你在VS Code里开发一个TypeScript项目,你使用的是TypeScript的代码编辑器、用TypeScript的扩展API、开发一个TypeScript项目——三层TypeScript环环相扣,构成了一个自洽的、彻底的世界。

在这个世界里,类型检查是原生的,智能提示是原生的,重构支持是原生的。没有额外的插件,没有额外的配置,你只需要写TypeScript,VS Code就会自动理解你要做什么,并在这个过程中变得越来越懂你的代码。

这是TypeScript工具链最理想的状态:语言、编译器、IDE,三位一体。它们之间没有翻译的损耗,没有理解的偏差。当你写代码的时候,你是在和一个完全理解你的系统对话。IDE知道你定义了什么类型,知道这些类型之间是什么关系,知道你接下来可能想做什么。这种理解不是靠模式识别和统计预测来模拟的——它是精确的、基于类型系统的、可靠的。


我有时候会想象一个画面。

一个叫小迟的年轻工程师,在凌晨一点打开电脑,准备修复一个困扰了他半天的bug。他的面前是一段Claude Code生成的代码——AI帮他写好了函数的框架,填充了基础的逻辑。代码看起来是对的,语法正确,逻辑通顺。但小迟知道,在TypeScript的世界里,看起来对是不够的。

他按下保存键。编译器开始工作。

屏幕安静了三秒钟。

然后,TypeScript的编译器在一个变量名下面画了一道红色的波浪线。鼠标移过去,信息浮现:Type 'string | undefined' is not assignable to type 'string'。 旁边还贴心地提示:Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Types of parameters ... are incompatible. Consider using an explicit type guard.

小迟笑了。他知道这个bug在哪里了。

他修正了那个可能为undefined的变量,加上了必要的空值检查,保存,编译,绿色通过。

这不是一个戏剧性的时刻。没有灵光乍现,没有漫长排查,没有半夜三更给同事发消息问这个接口是什么意思。TypeScript的骨骼已经提前说出了问题的位置。骨骼不说话,但它一直在那里守着。


写到这里,我想起文章开头那个深夜里的老周。他坐在屏幕前,蓝光把他的脸切成明暗两半,手指悬在键盘上方,不知道第2847行和第3012行之间到底隔了什么。

如果他能重来一次,他大概会选TypeScript。不是因为TypeScript能让他变成更好的程序员,而是因为TypeScript能让他不用在半夜三更靠直觉摸索一段没有形状的代码。骨骼给代码以形状,形状让错误有迹可循,有迹可循的错误在编译时就被解决了,而被解决的错误不会在凌晨三点叫醒任何人。

这大概是TypeScript最朴素的慈悲:它不是让编程变得更炫酷的语言,它是让编程变得更安静的语言。

安静不是无趣。安静是骨骼内在的质地,是水底下暗流的方向,是所有那些不需要说出来、但说出来就会更好的事情。

JavaScript是流动的。TypeScript给了这种流动一个方向。

以上,既然看到这里了,如果觉得不错,随手点个赞、在看、转发三连吧,如果想第一时间收到推送,也可以给我个星标⭐~

谢谢你读到这里。下次见。

查看文章页 ↗