TypeScript 就是在 JavaScript 之上加了一套静态类型系统。每个 JavaScript 程序都是合法的 TypeScript 语法;TypeScript 增加了类型注解、一个在代码运行前检查这些注解的编译器,以及一个再把它们移除的构建步骤。运行时只有 JavaScript,所以区别完全在于你在发布之前能发现什么。
在 JavaScript 中,同一个函数就是这段代码去掉 type Product = ...、: Product[] 和 : number。类型为编译器和编辑器提供了信息,但不会改变程序的行为。
TypeScript 与 JavaScript 一览
| JavaScript | TypeScript | |
|---|---|---|
| 类型系统 | 动态:类型属于值,只有在运行时才知道 | 静态:类型通过声明或推断得到,在编译期检查 |
| 类型错误何时出现 | 那一行运行时(undefined、NaN、TypeError) | 在编辑器中边写边提示,以及编译时 |
| 运行环境 | 浏览器、Node.js、Deno、Bun,直接运行 | 同样的环境,在移除类型之后运行 |
| 构建步骤 | 不需要 | tsc 或打包工具,或者自己去除类型的运行时 |
| 文件 | .js、.mjs、.cjs | .ts、.mts、.cts、.tsx,以及 .d.ts 类型声明 |
| 运行速度 | 基准 | 完全相同:输出就是 JavaScript |
| 编辑器支持 | 基于推断类型和库类型定义的自动补全,可能不完整 | 基于声明类型的自动补全、重命名和“查找所有引用” |
| 学习曲线 | 较低 | JavaScript 加上类型系统 |
| 标准 | ECMAScript,由 TC39 制定 | 微软的开源项目,跟进 ECMAScript |
同一段代码的两种写法
下面是一个 JavaScript 函数。其中没有任何地方说明 user 应该是什么样子:
function greeting(user) {
return `Hello, ${user.firstName} ${user.lastName}`;
}
greeting({ firstname: "Ada", lastName: "Lovelace" });
// "Hello, undefined Lovelace", no error anywhere
TypeScript 版本只需声明一次结构,拼写错误就会在代码运行之前被报告出来:
interface User {
firstName: string;
lastName: string;
}
function greeting(user: User): string {
return `Hello, ${user.firstName} ${user.lastName}`;
}
greeting({ firstname: "Ada", lastName: "Lovelace" });
// error TS2561: Object literal may only specify known properties,
// but 'firstname' does not exist in type 'User'. Did you mean to write 'firstName'?
语法上的区别就只有这些注解。TypeScript 也增加了一些自己的声明(interface、type、enum、Array<string> 这样的泛型、private 这样的访问修饰符),但语句、运算符和内置对象都是 JavaScript 的。
TypeScript 能发现而 JavaScript 发现不了的问题
JavaScript 会悄悄地转换类型。对于来自表单字段的值(它们总是字符串),这种 bug 很常见。运行下面的代码,看看编译器怎么说:
index.ts(7,17): error TS2345: Argument of type 'string[]' is not assignable to parameter of type 'number[]'.
Type 'string' is not assignable to type 'number'.
在 JavaScript 中这段代码能运行,并输出 010205,因为 0 + "10" 是字符串拼接。TypeScript 会拒绝编译,直到你把字符串转换成数字,例如使用 fromForm.map(Number)。
另一大类问题是可能缺失的值。Array.prototype.find 在没有匹配项时返回 undefined,TypeScript 会强制你处理这种情况:
index.ts(8,13): error TS18048: 'user' is possibly 'undefined'.
纯 JavaScript 版本会在运行时崩溃,报 TypeError: Cannot read properties of undefined (reading 'name')。TypeScript 中的修复方法,就是处理编译器指出的那种情况:
输出:
GRACE
no user with id 3
TypeScript 发现不了的问题有:逻辑错误(一个写错的公式类型也是对的),以及运行时进入程序的数据的任何问题。一个类型标注为 User 的 API 响应,只有在发送它的服务器正确时才是正确的,因为代码一运行,类型就不存在了。对这类数据,要用运行时代码来检查。
在 TypeScript 中使用 JavaScript 库
每个 npm 包都能在 TypeScript 中使用,因为输出本来就是 JavaScript。一个包的类型来自以下三处之一:
- 包自带
.d.ts文件。 大多数仍在积极维护的包都是这样,不需要额外安装任何东西。 - 单独的
@types包,来自社区项目 DefinitelyTyped:npm install --save-dev @types/lodash为lodash添加类型。 - 哪里都没有。 这时如果开启了
strict,导入语句本身就会报错:
error TS7016: Could not find a declaration file for module 'lodash'. '/project/node_modules/lodash/lodash.js' implicitly has an 'any' type.
Try `npm i --save-dev @types/lodash` if it exists or add a new declaration (.d.ts) file containing `declare module 'lodash';`
修复方法是安装对应的 @types 包(如果存在的话),或者自己在 .d.ts 文件中描述这个模块;声明文件页面 介绍了具体做法。
构建步骤
浏览器和 Node.js 不做类型检查,所以 TypeScript 需要在源码和实际运行的代码之间加一个步骤。常见的配置有三种:
- 用
tsc编译所有文件。 它做类型检查并写出.js文件,通常放在dist文件夹中。简单,也是库的标准做法。 - 打包工具或开发服务器移除类型,
tsc --noEmit负责检查。 Vite 和 esbuild 只去除类型而不检查,这让热更新保持快速;类型检查交给编辑器和 CI 步骤。 - 由运行时移除类型。 当前版本的 Node.js、Deno 和 Bun 都能直接运行
.ts文件。它们在运行时都不做类型检查,所以仍然要靠tsc --noEmit(或deno check)来发现类型错误。
JavaScript 完全不需要这些,这是它在小脚本上最大的实际优势。TypeScript 这一步的代价主要是配置(一个 tsconfig.json 和一个 typescript 开发依赖)以及编译时间;TypeScript 7 的原生编译器在大型项目上把编译时间缩短到了大约十分之一。
学习曲线
你掌握的所有 JavaScript 知识都可以沿用,因为 TypeScript 的运行时就是 JavaScript。新的内容是类型系统,可以分层学习:
- 给变量、参数和返回值加注解(
: string、: number[])。 - 用
interface和type定义对象类型、可选属性,以及string | number这样的联合类型。 - 收窄:用
typeof、in或===检查一个值,让编译器知道你处于哪种情况。 - 泛型、
Partial<T>和Pick<T, K>这类工具类型,以及供库作者使用的高级类型。
前两层就覆盖了大部分应用代码。很多类型都是推断出来的,所以大量 TypeScript 代码看起来就像 JavaScript,只在函数签名上有注解。
何时选择 TypeScript,何时选择 JavaScript
TypeScript 比 JavaScript 好吗?对于多人维护或要存在多年的代码,通常是的,整个行业也正朝这个方向发展:按 GitHub 每月贡献者数量统计,TypeScript 在 2025年8月超过了 JavaScript 和 Python,成为 GitHub 上使用最多的语言。对于小脚本,纯 JavaScript 往往是更合适的工具。
以下情况选择 TypeScript:
- 代码由不止一个人维护,或者要维护几个月甚至几年。
- 代码库大到你无法记住每个函数签名。
- 你经常重构:重命名一个属性会更新所有用到它的地方,编译器还会列出遗漏之处。
- 你要发布一个库:
.d.ts文件能为使用者提供自动补全和检查。 - 框架要求使用它。Angular 应用用 TypeScript 编写,Next.js 和 Astro 默认用 TypeScript 创建新项目,Vite 的 React、Vue 和 Svelte 模板也都有 TypeScript 版本。
以下情况选择 JavaScript:
- 程序是一个简短的脚本、一次性的实验,或者在浏览器控制台里运行的代码片段。
- 你第一次学编程,希望一次接触的概念少一些。
- 没有构建步骤,你也不想要构建步骤。即便如此,在普通
.js文件中使用// @ts-check配合 JSDoc,也能得到一定的检查。
把 JavaScript 项目迁移到 TypeScript
迁移不必一次完成。编译器可以接受与 TypeScript 并存的 JavaScript:
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
开启 allowJs 后,.js 文件也会被编译,并且可以从 .ts 文件导入,反之亦然。然后逐步转换:
- 把一个文件从
.js重命名为.ts,修复编译器在其中报告的错误。 - 从依赖树的末端开始(导入较少的工具模块),再逐步向内推进。
- 开启
checkJs,或者在单个.js文件顶部加上// @ts-check,对还没有重命名的文件做类型检查。
在开启检查的 JavaScript 文件中,类型由 JSDoc 注释提供:
// @ts-check
/**
* @param {number} price
* @param {number} qty
* @returns {number}
*/
function lineTotal(price, qty) {
return price * qty;
}
lineTotal("3", 2);
// error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
有些团队就停在这一步:JavaScript 文件配合 JSDoc 类型,由 tsc 检查,自己的代码不需要构建步骤。另一些团队则一路转换到 .ts。如果在已有项目中开启 strict,一开始会看到大量错误;严格模式页面 列出了每个标志检查的内容,方便你逐个开启。
常见问题
TypeScript 和 JavaScript 的主要区别是什么?
TypeScript 给 JavaScript 加上了静态类型。你描述每个值是什么(name: string、items: Item[]),TypeScript 编译器会在代码运行之前报告错误。JavaScript 不做任何提前检查:类型错误只有在那一行执行时才会暴露,通常表现为 undefined 或 TypeError。
TypeScript 比 JavaScript 好吗?
对于大多数由多人维护、或者会存在几周以上的项目来说,是的:类型能发现一整类 bug,让重构变得安全,还能为编辑器的自动补全提供支持。对于简短的脚本、快速原型或练习题,纯 JavaScript 上手更快,也不需要配置构建。
TypeScript 比 JavaScript 运行得更快吗?
不会,也不会更慢。TypeScript 编译成 JavaScript,类型会被擦除,所以运行的代码就是你手写也会写出的那份 JavaScript。唯一的额外开销是开发时的编译时间。
应该先学 JavaScript 还是 TypeScript?
先学 JavaScript 基础,或者和 TypeScript 一起学。所有运行时行为(变量、函数、对象、数组、Promise)都是 JavaScript 的,TypeScript 只是描述它们。只要你能写小型 JavaScript 程序,再加上类型就只是一小步。
可以在同一个项目中同时使用 TypeScript 和 JavaScript 吗?
可以。在 tsconfig.json 中设置 "allowJs": true,编译器就会接受与 .ts 文件并存的 .js 文件。再加上 "checkJs": true(或者在每个文件中加 // @ts-check 注释),就能利用 JSDoc 注释中的类型对 JavaScript 文件也做类型检查。这是逐个文件迁移项目的常用方法。