FlowingDev

TypeScript 解密:JavaScript 是如何装上安全带和安全气囊的

了解为什么说 TypeScript 是带超能力的 JavaScript,它通过添加静态类型来及早发现错误,让代码更健壮、更易于理解。

试用工具: TypeScript Playground

一言以蔽之

TypeScript 是一种基于 JavaScript 构建的编程语言,它增加了一套“类型系统”,就像一个为你数据准备的语法检查器,能在代码运行前就捕获一大堆 bug。

它解决了什么问题

想象一下 Web 的早期岁月。JavaScript 的诞生非常仓促(传说只用了 10 天),就是为了让网页能多一点交互性。它本是为小型脚本设计的:给按钮加个动画、验证个表单。它动态、灵活、宽容。这种宽容曾是它最大的优点,但随着 Web 的发展,也成了它最大的缺点。

在原生 JavaScript(vanilla JavaScript)里,一个变量可以随心所欲地成为任何东西。它可以一开始是数字,然后被重新赋值为字符串,接着变成一个对象,最后又成了 undefined。

let myThing = 123;         // myThing 是个数字
myThing = "Hello world"; // 现在是个字符串了
myThing = { id: 456 };   // 然后,又成了个对象

对于一个 20 行的脚本来说,这没问题。但对于一个支撑像 Google Maps 或 Figma 这种大型服务的 20 万行应用来说,这简直是混乱的温床。开发者们会把错误类型的数据传给一个函数,导致在运行时——也就是在真实用户面前——爆出那个臭名昭著的 undefined is not a function 错误。重构代码简直是一场噩梦;你改一个函数,可能会在不知不觉中弄坏应用的其他十个地方,直到为时已晚。

微软在自家的大型 JavaScript 项目中就遇到了这个问题,于是他们决定为这种混乱带来一些秩序。2012 年,他们发布了 TypeScript。目标不是取代 JavaScript,而是增强它。它将在人人都已熟悉的语言之上,增加一层安全性和可预测性,使其适用于现代 Web 所需的大型、复杂应用。

底层工作原理

TypeScript 的魔法发生在代码到达浏览器之前。你的浏览器并不运行 TypeScript,它运行的是纯正的老式 JavaScript。TypeScript 的工作是在开发期间充当一个超级智能、主动出击的 linter(代码检查工具)。

### TypeScript 是 JavaScript 的超集

这是需要掌握的最重要的概念。“超集”意味着任何有效的 JavaScript 代码同样也是有效的 TypeScript 代码。你可以拿一个 .js 文件,把它重命名为 .ts,它照样能用。TypeScript 只是在 JavaScript 的基础上增加了新语法。

最主要的补充就是类型注解(type annotations)。你现在可以告诉 TypeScript 你期望什么类型的数据。

// 这是纯 JavaScript
function getLength(someValue) {
  return someValue.length;
}

// 这是 TypeScript
function getLengthTyped(someValue: string | any[]) {
  return someValue.length;
}

在 JavaScript 版本中,如果你调用 getLength(42),你会得到一个运行时错误,因为数字 42 没有 length 属性。而在 TypeScript 版本中,你的代码编辑器和编译器会立刻对你尖叫,告诉你数字不是一个有效的参数。这个 bug 在你保存文件之前就被捕获了。

### 转译过程

那么,如果浏览器不运行 TypeScript,那些额外的语法去哪了呢?它们在一个叫做 转译(transpilation) 的过程中被抹掉了。

转译是一种特殊的编译:一种源码到源码的转换。TypeScript 编译器(一个通常叫做 tsc 的程序)会读取你的 .ts 文件,检查其中的类型错误,然后吐出一个干净的、任何浏览器都能理解的 .js 文件。

下面是一个前后对比:

TypeScript 输入 (.ts)

function greet(person: string, date: Date): string {
  return `Hello ${person}, today is ${date.toDateString()}!`;
}

JavaScript 输出 (.js)

function greet(person, date) {
  return "Hello ".concat(person, ", today is ").concat(date.toDateString(), "!");
}

注意类型注解(: string, : Date)是如何完全消失的。转译器用它们来检查你的工作,然后就把它们扔掉了,留下了可执行的 JavaScript。一个 TypeScript playground 会在浏览器里即时完成这个转译步骤,所以你可以实时看到结果。

### 类型检查与推断

TypeScript 的“大脑”是它的类型检查器。它负责分析你的代码并验证类型是否匹配。但你并不总是需要亲手写出类型。TypeScript 足够聪明,很多时候能通过一个叫做**类型推断(type inference)**的过程自己弄明白。

let name = "Alice"; // TypeScript 推断出 `name` 的类型是 `string`
let age = 30;     // TypeScript 推断出 `age` 的类型是 `number`

// 如果你这么做,TypeScript 会报错:
// Type 'number' is not assignable to type 'string'.
name = 42;

你并不需要写 let name: string = "Alice"。TypeScript 看到你赋了一个字符串字面量,就正确地推断出了类型。这让你的代码保持简洁易读,同时又能享受到完整的安全保障。你只需要在 TypeScript 无法自行判断时才添加明确的类型,比如函数参数或未赋值的变量。

真实世界的故事

### 重构大作战

一家创业公司的旗舰产品是一个庞大的 JavaScript 单体应用。早期团队追求快速迭代,打破常规,现在新团队正在为此付出代价。他们需要重构一个处理用户权限的核心模块。问题是,没人确定 user 对象应该是什么“形状”的。它是有 user.isAdmin 还是 user.role === 'admin'?user.id 是数字还是字符串?

每一次修改都是一场赌博。他们决定引入 TypeScript,一个文件一个文件地来。他们从定义一个清晰的 User 接口开始:

interface User {
  id: number;
  email: string;
  role: 'admin' | 'editor' | 'viewer';
  lastLogin: Date;
}

突然间,他们的 IDE 像圣诞树一样亮了起来,指出了代码库中每一处错误使用 user 对象的地方。一个函数试图访问 user.isAdmin,但这在他们的新接口上不存在。另一个函数传入了字符串 ID 而不是数字。这场重构从一场可怕的猜谜游戏,变成了一个系统化、有指导的过程。

经验: TypeScript 通过为你的数据结构创建唯一的真实来源(single source of truth),将混乱的重构变为一项安全、可预测的工程任务。

### 幽灵 API Bug

一名前端开发者正在构建一个仪表盘,用于显示来自第三方 API 的数据。API 文档清楚地说明,产品的价格会是一个数字,比如 19.99。她据此编写了 TypeScript 代码:interface Product { name: string; price: number; }。

她的代码运行良好数周,直到有一天,一个用户报告说“总计”的计算结果显示为 NaN(Not a Number,非数字)。经过数小时的调试,她发现对于一个特定且罕见的产品,API 返回的价格是字符串格式:"19.99"。在 JavaScript 中,10 + "19.99" 的结果是字符串 "1019.99",这破坏了她后续的计算。

有了 TypeScript,她本可以在源头就捕捉到这个问题。通过用她的 Product 接口验证传入的 API 数据,类型不匹配会在坏数据进入她系统的瞬间就抛出错误,而不是在十步之后尝试进行数学运算时才发现。

经验: TypeScript 强制执行数据契约,使你的应用对来自外部 API 的意外更改或错误更具弹性。

常见的误区和陷阱

  • any 这个“逃生通道”: TypeScript 有一个特殊的类型叫做 any,它基本上是在告诉编译器“相信我,我知道我在干嘛”,并关闭对该变量的所有类型检查。过度使用 any 就像买了一辆以安全功能为卖点的车,然后把所有安全功能都关掉一样。这完全违背了使用 TypeScript 的初衷。
  • 忽略编译器错误: 有时你会看到 TypeScript 报出一条红色的波浪线,但转译后的 JavaScript 似乎运行正常。这时很容易想忽略这个错误。这是个陷阱。这个错误是一个潜在运行时 bug 的警告信号。听听你的编译器怎么说;它在努力帮你。
  • 与类型系统“对着干”: 新手常常因为 TypeScript 不让他们做某件事而感到沮丧。他们不去思考正确的类型定义方式,而是试图用类型断言(as any)或其他变通方法来“欺骗”编译器。这通常意味着他们对数据流的理解有缺陷,而编译器正在正确地指出问题。
  • 忘记 @types 包: 许多流行的 JavaScript 库本身不包含类型定义。TypeScript 社区维护着一个庞大的类型定义仓库,叫做 DefinitelyTyped。如果你正在使用像 Lodash 或 Express 这样的库,你通常需要安装一个相应的 @types/lodash 包来获得正确的自动补全和类型检查。

为什么你应该关注它

当出现以下情况时,你应该选择(或至少考虑)TypeScript:

  • 你的 JavaScript 项目不止几个文件。
  • 你在团队中工作,需要确保每个人对数据结构的认知保持一致。
  • 你发现自己花在调试 undefined 错误上的时间比写新功能还多。
  • 你希望你的代码编辑器变成一个超级助理,提供令人难以置信的自动补全、重构工具和内联错误提示。
  • 你正在构建一个需要长期维护和扩展的应用。

简而言之,TypeScript 是现代 JavaScript 开发的专业标准。学习它是一项能值回票价的投资,它会通过减少 bug、简化协作和带来更自信的开发体验来回报你。

深入探索

编辑器如何理解代码

语法颜色来自语言解析器,而不是程序执行。CodeMirror 和 Lezer 在浏览器本地识别符号和语法结构。建议结合关键字、受支持的局部名称、代码片段和内置的小型参考库;悬停在已收录的符号上可查看签名。建议并不保证运行时真的提供该 API。解析器在支持时检测错误语法,但不能替代类型检查或实际执行:语法正确的代码仍然可能失败。Ruby 和 Lua 使用基于词元的模式,诊断能力较有限。没有远程语言服务器,源代码无需离开设备。

CodeMirror & Lezer: MIT.

理论搞定,动手试试吧——100% 在你的浏览器中运行。

试用工具: TypeScript Playground