ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

TypeScript与JavaScript本质区别:类型系统如何改变前端开发

TypeScript与JavaScript本质区别:类型系统如何改变前端开发 最近帮团队做前端技术面试凡是简历里写了“熟练掌握JavaScript”的同学我基本都会追问同一个问题你觉得 TypeScript 和 JavaScript 最大的区别是什么大部分人的第一反应是“TS有类型”但继续问“这个类型系统到底帮你解决了什么问题”时能讲透的人其实不多。这篇文章我想从一个整天跟这两种语言打交道的开发者视角把 TS 和 JS 的区别拆开讲清楚。适合刚学完 JS 基础、准备入 TS 的初学者也适合在项目中犹豫要不要从 JS 迁移到 TS 的团队。我会从运行机制、类型系统、错误暴露时机、语法扩展、工程生态、迁移路线和面试表达七个角度来对比尽量不写成官方文档的翻译。1. 血缘关系先讲清TS是JS的超集但不是“另一门语言”1.1 从编译产物看两者关系JavaScript 是 ECMAScript 规范的一种实现运行在浏览器、Node 等 JavaScript 运行时里。TypeScript 是微软开源的一门编程语言官方定位非常明确JavaScript 的超集。这里的“超集”意思是所有符合规范的 JS 代码在语法层面上同样是一份合法的 TS 代码TS 只是在这个基础上增加了类型标注和相关语法扩展。所以两者不是并列关系更不是竞争关系。你写 TS 的时候实际上也在写 JS。这一点最直观的证据是编译产物。TS 代码通过官方编译器tsc编译后输出的是普通 JS 文件所有类型相关的部分会被擦除。比如这段 TSinterface User { name: string; age: number; } const user: User { name: Tom, age: 18 }; console.log(user.name);编译完之后就是const user { name: Tom, age: 18 }; console.log(user.name);从这个例子可以看出TS 在运行时层面上不会给 JS 增加任何额外逻辑最终真正运行的东西仍然是 JS。这也解释了很多人关心的性能问题TS 代码跑起来之后和手写 JS 没有本质差别类型检查只是发生在编译阶段不会让线上代码变慢。1.2 浏览器不认TS运行前必须转译JS 可以直接被浏览器或 Node 等运行时加载执行而 TS 不行。主流流程是先把 TS 转译成 JS再交给运行时去跑。转译工具有很多选择官方tsc、Babel、esbuild、SWC、ts-node 等。这些工具的分工略有不同我后面在工程化章节再展开。这里面有一个常见误解以为 TS 必须搭配很重的构建链路。其实如果你只是写一个很小的脚本完全可以用tsc单文件编译或者用tsx这类工具直接运行 TS 文件。只不过现实中的前端项目本来就有一套构建流程把 TS 编译这步融合进去并没有额外增加太多成本。顺带说一句网上偶尔能看到类似typescript [{}]的奇怪写法其实 TypeScript 并不是一个浏览器端的全局对象而是一个 npm 包的名称、一个命令行工具。它负责做语法转换和类型检查不会出现在你的运行时代码里。分清“语言”和“工具链”这两个概念对理解两者的关系很有帮助。1.3 版本推进TS 和 ECMAScript 规范并不同步TS 的版本号与 ECMAScriptES标准并不一一对应。TS 团队经常提前实现尚未进入标准的语法提案比如早期的可选链?.、空值合并??、装饰器等都是先在 TS 里可用之后才被 JS 标准吸收。这也是“超集”的另一层含义TS 会领先于标准但也会跟随标准的变化做调整。需要特别注意的是有一部分 TS 自带语法并不属于 JS 标准比如enum、namespace、declare等。它们要么被编译成普通 JS 对象要么只存在于类型世界中。理解这一点之后你就不会在“TS 和 JS 语法哪个多”这个问题上犯迷糊TS 的语法是 JS 的超集多出来的部分有些会被 JS 学习有些会一直作为 TS 自己的特性存在。下面放一张总览对比表后面每一章都会围绕这些维度展开对比维度JavaScriptTypeScript语言类型动态类型静态类型含类型推断运行方式浏览器/Node 等直接运行编译为 JS 后运行类型检查时机运行时开发期/编译期是否携带类型信息不携带携带编译后擦除IDE 智能提示较弱强提示、安全重构语法范围遵循 ECMAScriptJS 超集含 enum/namespace/泛型等学习曲线低中等适用场景小脚本、原型、快速迭代中大型项目、长期维护、团队协作2. 类型系统是最大分水岭静态检查如何改变写代码的方式2.1 从“运行时才知道”到“写的时候就知道”JS 是动态弱类型语言。变量最终是什么类型要等代码运行的那一刻才知道。这带来了极大的灵活性但也埋下了不少坑。比如这段 JSfunction add(a, b) { return a b; } console.log(add(1, 2)); // 3 console.log(add(1, 2)); // 12这段代码在 JS 里完全合法也都能执行但结果截然不同。如果函数本意是做数字相加第二个调用就是在类型没有被约束的情况下悄悄埋 bug。这种问题在纯 JS 项目里特别容易出现在公共函数、接口数据、表单提交这些“类型随时在变”的场景中。TS 解决的不是“避免一切错误”而是把这一类错误前置。同样是上面的函数写成function add(a: number, b: number): number { return a b; }再传一个字符串进去编译器会直接标红。类型系统让“类型不符合预期”这件事在写代码时就被发现而不是等接口联调或者用户反馈才暴露。这里要避免一个误区类型检查不等于运行时代码防御。TS 的类型会在编译后消失所以它不会为你在运行时拦截非法参数。如果你需要运行时校验还是要写防御逻辑或用专门的校验库。TS 的核心价值是“在编译期约束内部调用关系”它替代的是代码审查里的一部分人工检查而不是替代数据校验。2.2 类型注解、类型推断与“活文档”TS 当然支持显式类型注解但它真正强大的是类型推断。你写let count 1TS 会自动把count推断为number你写const user getUser()TS 会根据函数返回值推断出user的结构。所以并不是每行代码都要手动标注很多情况下类型是“自动浮出来”的。这让 TS 代码具备了一个很实用的属性自描述。一个函数的入参、返回值、可能的空值情况通过类型签名就能看清大半。相比那些可能过期的注释文档类型是编译器强制维护的文档IDE 里悬停一下就能看到结构。这一点对新人接手老项目尤其友好很多本来要读半天源码才能确认的约定类型签名直接写明白了。不过类型设计也需要经验。如果你把函数参数都定义成any或者把所有对象都描述成宽泛的{}那类型文档的价值就大打折扣。我个人在 code review 时最关注的就是公共 API 的类型签名。因为内部实现改起来相对容易但公共类型一旦传播开后面想收紧会非常痛苦。2.3 联合类型、接口和泛型带来的表达力JS 代码里描述“这个参数可能是字符串也可能是数字”通常靠注释和typeof判断。TS 用联合类型直接表达type Id string | number; function formatId(id: Id) { return #${id}; }描述一个对象的结构用interface或typeinterface User { name: string; age: number; email?: string; } function greet(user: User) { return Hello ${user.name}; }描述“数组里装的是同一类东西”用泛型function firstItemT(list: T[]): T | undefined { return list[0]; }这些能力在 JS 里没有直接的运行时对应物。你可以说它们只是“类型提示”但它们改变了团队协作时对函数边界的理解方式。以前你调用一个函数要翻文档、试参数现在编译器直接告诉你要传什么、会返回什么调用处的智能补全也精确得多。这种体验上的差距一旦习惯就很难退回纯 JS。3. 错误发生的时机为什么TS能把“运行时炸弹”提前拆掉3.1 运行时错误在编译期被拦截“javascript 运行时报错”一直是个高频搜索词恰好反映了纯 JS 开发中一个很痛的现状大量错误只能等代码跑起来才暴露。最常见的几类包括Cannot read properties of undefined、undefined is not a function、xxx is not defined等等。这类问题在做复杂业务、操作深层对象属性时尤其容易发生。TS 在开启strict模式后能够拦截掉其中相当一部分。比如你从配置对象里取一个可选属性编译期就会提示可能为undefinedtype Config { timeout?: number; }; function start(config: Config) { config.timeout.toFixed(); // TS 报错timeout 可能为 undefined config.timeout?.toFixed(); // 使用可选链后通过 }这个提示的成本极低收益却很高。以前你可能要等测试环境复现、加日志、定位数据来源现在写代码的瞬间就能发现访问路径不安全。当然TS 不能拦截所有运行时错误比如后端返回了与类型声明完全不符的数据所以它不能替代异常监控和测试但它可以把错误防线大幅度前移。3.2 strictNullChecks可空性检查是TS的灵魂很多团队虽然用了 TS但tsconfig.json里没有开启strict结果类型检查形同虚设。strict下面包含多个子选项其中最影响日常体验的是strictNullChecks。开启它之后null和undefined不再能随意赋给其他类型。比如let name: string null; // 报错 let age: number undefined; // 报错这逼迫你在代码中显式处理“可能为空”的情况。刚开始会觉得很烦总觉得运行时不会为空经历几次线上空指针之后你会理解这种“烦”其实是在帮你排除隐患。处理可空值的手段也很成熟可选链?.、空值合并??、typeof/instanceof判断、自定义类型谓词is等。等你习惯了这些写法再回到纯 JS 项目会特别没有安全感——因为你又回到了“只能靠肉眼和人脑保证属性存在”的状态。3.3 重构从全局搜索到编译器保证JS 项目里改一个对象字段名常规操作是全局搜索、肉眼排查、逐个替换。小型项目还好到了中大型项目一个字段可能在几十个文件里被引用漏掉任何一个都不报错直到运行时某个页面白屏。这个问题在 TS 项目里被系统性缓解了IDE 的类型感知重命名会高亮所有引用位置编译器也会在编译时检查字段名是否匹配。函数签名变化同理。把function fetchList(page, size)改成function fetchList(params: { page: number; size: number })所有调用处都会立刻标红。这种“可验证的改动”对长期维护极其重要。我见过不少团队引入 TS 后最明显的感受不是“写代码变慢了”而是“改代码敢改了”。类型的价值很多时候体现在你不需要记住所有调用关系编译器会替你盯着。4. 语法层面的差异不只是类型枚举、命名空间与最新特性4.1 TS独有的语法清单除了interface、type、泛型这类类型相关语法TS 还有一些运行时语法的扩展。比较典型的有enum枚举编译成一个 JS 对象。namespace命名空间早期用来组织模块代码。declare声明文件.d.ts描述已有 JS 代码的类型。abstract class抽象类不能直接实例化。装饰器一种改变类或类成员行为的语法NestJS、Angular 等框架重度使用。const enum编译后会被直接替换为字面量不生成运行时对象。这些特性在纯 JS 里不存在或者说只能靠约定去模拟。比如你要在 JS 里表示一组常量通常写const Color { RED: red, GREEN: green }没有类型层面的约束在 TS 里用enum或const enum值会被约束在枚举范围内。另外要留意装饰器的特殊历史ECMAScript 标准装饰器与 TS 早期的装饰器实现语义并不完全一致很多框架当前用的还是 TS 实现未来切换标准装饰器时要注意兼容性。4.2 declare global扩展全局对象类型很多人搜过“typescript 命名空间 declare global”这里展开举个例子。假设你需要在全局对象window上挂一个自定义属性__themeexport {}; // 确保当前文件是一个模块 declare global { interface Window { __theme: light | dark; } }之后在业务里写window.__theme light就不会报类型错误而且 IDE 会给属性名和取值提供补全。这在纯 JS 项目里只能靠注释和团队约定TS 把它变成了编译器可检查的规范。全局类型的扩展能力在后端 Node 项目、浏览器插件、SSR 这些场景里都很常用。4.3 JS现有语法在TS中的增强从剩余参数到模板字面量类型JS 本身的语法在 TS 里同样会被类型系统增强。比如“javascript 剩余参数”这个搜索词在 JS 里是function sum(...nums) { }TS 可以进一步约束参数类型function sum(...nums: number[])。这样调用时传了字符串编译器会直接报警。“javascript 合并字符串”在 JS 里是模板字符串${a}-${b}TS 里还可以定义模板字面量类型type EventName on${Capitalizestring}; // 合法取值onClick, onChange, onScroll...“javascript 通过字符串调用函数”这种需求JS 里通常用对象映射来做TS 里可以用索引签名或联合类型约束允许的字符串键避免手滑调用一个不存在的函数const handlers: Recordopen | close, () void { open() {}, close() {}, };这个规律值得记一下JS 处理的是“值”TS 处理的是“值的类型”。你之前在 JS 里怎么组织代码TS 就提供对应的方式去约束和验证这套组织方式。5. 工程化生态对比框架、构建工具与版本兼容那些事5.1 主流框架对TS的支持深度现在的前端生态已经基本完成对 TS 的“收编”。Angular 从诞生第一天就是 TS 驱动的Vue 3 整个源码都用 TS 编写React 官方文档同样推荐 TS。可以说现代前端框架里 TS 几乎成了默认选项。但这并不代表第三方库都自带完美类型。很多老牌库还是用纯 JS 编写需要在项目里安装types/xxx还有一些小众库连types都没有你就得自己补一个.d.ts声明文件把模块描述成需要的结构。即使是这样TS 仍然值得引入。原因在于类型缺失只是“开发体验差一点”并不影响最终编译运行。反过来如果一个库的类型定义设计得好它在 IDE 里的使用体验甚至会超过文档。搜索热词里出现的splat.js这种“纯 JavaScript WebGPU 的 3D 高斯泼溅处理方案”也说明底层算法库完全可以用纯 JS 写上层业务逻辑用 TS 去约束两者并不冲突。TS 是开发期工具不是运行时的竞争者。5.2 构建链路esbuild/SWC做转译tsc做类型检查现代 Vite 工程在启动时用 esbuild 把 TS 转换成 JS速度极快。但 esbuild 默认只做语法转换和类型擦除并不做类型检查。所以你经常看到的现象是编辑器里已经标红了页面却还能正常跑起来。这其实不是 bug而是工具职责分离的结果。为了在 CI 阶段拦住类型错误工程里通常会单独跑一遍官方编译器npx tsc --noEmit这条命令只做类型检查不输出 JS 文件。团队入口脚本里通常会包含lint、typecheck、build三步各司其职。理解了这个分工你就不会因为“Vite 没报错但 tsc 报错”而感到困惑了。5.3 tsconfig配置与版本兼容的坑TS 的配置项非常多但刚开始只需要抓住几个关键项就行strict决定静态检查强度target决定转译后的语法版本module决定模块规范paths做路径别名。配置项是会演化的比如社区最近在讨论baseUrl被弃用的消息——有说法称在未来的 TS 7.0 中baseUrl可能停止生效官方更推荐用paths的相对路径或包名导入来替代。这意味着升级 TS 大版本时要留意配置文件的变化不能无脑升级。另一个典型问题是工具链版本匹配。热词里有“vue 类型工具与现有 typescript 7 不兼容”这样的讨论实际上很多框架的类型增强插件都需要特定范围的 TS 版本。遇到升级后类型报错或提示失效优先检查 TS、框架插件和类型工具三者的版本兼容范围大多数情况能通过对齐版本解决。这种“工程生态的摩擦”是选择 TS 时必须承担的成本但它通常是一次性的。6. 从JS项目迁移到TS的实操路线6.1 渐进式迁移而不是推翻重来把一个大项目从 JS 迁移到 TS最容易犯的错误是想“一步到位”。正确做法是渐进式迁移。第一步先把tsconfig.json配好允许 TS 和 JS 共存{ compilerOptions: { allowJs: true, checkJs: false, strict: true, outDir: dist }, include: [src] }allowJs: true让.js文件可以继续参与编译checkJs: false暂时不去检查 JS 文件。接下来把所有入口文件改成.ts再按照依赖关系从底层向上迁移。新写的文件直接用 TS老文件逐渐改造。如果某个文件暂时改不动可以在文件顶部加// ts-nocheck临时屏蔽类型检查等技术债允许时再回来处理。6.2 迁移时最常见的报错和处理方式迁移过程中见的报错翻来覆去就那么几类。第一类是隐式 any。函数参数没有类型TS 在严格模式下会报错。解决办法就是给参数和返回值补上类型如果实在不想管可以临时关闭noImplicitAny但我不建议长期这么做。第二类是空值检查。原来的 JS 代码可能大量使用“可能为 undefined”的属性开启严格空值检查后全线标红。这时需要逐个判断哪些真的可能为空用可选链或空值合并处理哪些确定不为空可以用类型断言或提前判空。第三类是第三方库缺少类型声明。优先去 npm 上搜types/库名没有的话写声明文件declare module some-lib;第四类是模块解析问题。moduleResolution配置和实际模块规范不匹配会出现导入路径报错。根据项目使用的 ESM 还是 CommonJS 去调整module和moduleResolution即可。6.3 团队协作规范TS 上线之后需要约定一些基本规范开启strict、不给新代码滥用any、公共函数的参数和返回值必须有明确类型、依赖第三方库时优先选择自带类型的版本。我的经验是团队里最好有一个人专门负责类型方案的设计。因为类型写不好带来的问题不是“运行不了”而是“类型意义不大”这种软性负担比编译错误更难发现。类型设计得好重构时编译器能帮上大忙设计得烂反而会让大家产生“TS 真麻烦”的抵触情绪。7. 面试被问到TS和JS区别这样答最加分7.1 一个高信息量的回答结构“typescript面试”是个热门搜索词可见这个问题出现频率很高。我的建议是不要只背结论而是分层回答第一层说定义TS 是 JS 的超集核心是静态类型系统代码需要编译成 JS 后运行。第二层说关键区别JS 在运行时确定类型TS 在开发期和编译期做类型检查能把相当一部分错误提前拦截。第三层说工程价值类型系统带来 IDE 智能提示、安全重构、可读性和可维护性的提升团队协作时尤其明显。第四层说代价TS 增加了编译步骤、学习成本和类型定义维护成本不是所有场景都必需。第五层结合实际讲一个例子某次线上 bug 是因为对象属性可能为空使用 TS 后这类问题在开发阶段就被编译器挡住了。这个结构既有理论也有实践比单纯说“一个强类型一个弱类型”要立体很多。7.2 什么时候仍然应该选择纯JS不能因为 TS 是主流就否定纯 JS 的适用场景。快速原型、Hackathon、单文件小工具、对构建产物体积极其敏感的环境这些情况下引入 TS 可能反而成为负担。团队如果完全没有 TS 基础且项目生命周期很短用纯 JS 也是理性的选择。说白了选型是一个权衡问题TS 的收益前端不明显后端才显现出来。如果项目要维护三个月以上、参与人数超过两个人TS 通常值得投入如果是明天就要交付的演示页面那 JS 的直接交付速度可能更重要。我在实际项目里的体会是真正让人想引入 TS 的契机往往是某次改了一个字段导致线上报错或者接手的旧项目完全看不懂函数调用关系。痛过之后自然就能理解 TS 的价值。7.3 写在最后的个人经验回到文章开头的问题TS 和 JS 最大的区别是什么我现在通常会这样回答JS 是一门活在运行时的语言它给你最大自由度也把问题留到最后一刻TS 是在它外面加了一层静态检查机制让你在写代码时就发现错误。TS 不会取代 JS它只是让 JS 这门语言更适合大规模、多人的工程协作。如果你刚准备学 TS我不建议从头啃一遍官方文档。找一个你写过的、不超过几百行的 JS 小项目试着把它迁移到 TS跑通strict模式再回头读一遍自己的代码。你会发现那些容易出错的边界情况在迁移过程中会一个接一个地被编译器找出来。这种体验比看教程来的直接也更能理解为什么现在的开源项目几乎都在拥抱 TS。等到你真正习惯了类型带来的安全感可能就再也不想回到纯 JS 的状态了。
返回列表