TypeScript 和 JavaScript 到底差在哪儿?为啥越来越多项目从 JS 迁移到了 TS?用了大半年 TS 后,慢慢摸透了这两门语言的核心差异,不只是多了几个类型注解那么简单。
一、类型系统:从「动态宽松」到「静态严格」
JavaScript 最显著的特点就是动态类型 ------ 变量类型可以随时变化,声明时不用指定类型,运行时才确定。比如:
js
let num = 123;
num = "hello";
num = { name: "test" };
这种灵活性在小脚本里很方便,但项目大了就容易出问题。比如写了个计算函数,本来该传数字,结果传了字符串,JS 不会提前提醒你:
js
function add(a, b) {
return a + b;
}
add("1", 2); // 返回 "12",可能不是预期结果
TypeScript 则引入了静态类型系统,变量声明时可以指定类型,且类型一旦确定(除非用 any)就不能随意更改。上面的函数用 TS 写会变成:
ts
function add(a: number, b: number): number {
return a + b;
}
add("1", 2); // 直接报错:类型"string"不能赋给类型"number"
TS 的类型远不止基本类型,还能定义接口、联合类型、交叉类型等复杂结构。比如定义一个用户类型:
ts
interface User {
name: string;
age: number;
isAdmin?: boolean; // 可选属性
}
const user: User = {
name: "张三",
age: 25
// 少个属性或类型不对都会报错
};
联合类型能让变量具备多种可能的类型,比如一个既能是字符串也能是数字的变量:
ts
let data: string | number;
data = "hello"; // 合法
data = 123; // 合法
data = true; // 报错:类型"boolean"不能赋给类型"string | number"
交叉类型则可以组合多个类型的特性,比如同时拥有用户和权限信息的对象:
ts
interface User {
name: string;
}
interface Permissions {
canEdit: boolean;
}
type UserWithPermissions = User & Permissions;
const user: UserWithPermissions = {
name: "李四",
canEdit: true
};
这种类型约束让代码更健壮,尤其在多人协作时,类型定义本身就是一种清晰的文档。
二、代码检查:从「运行时报错」到「编译时预警」
JavaScript 的错误往往要等到代码运行时才会暴露。比如访问一个不存在的对象属性:
js
const user = { name: "李四" };
console.log(user.age); // undefined,不报错
如果是在复杂业务逻辑里,这种隐性错误很难排查。比如在处理后端返回数据时,经常会因为字段名拼写错误导致问题:
js
// 后端返回数据格式:{ userName: "张三", userAge: 25 }
function formatUser(user) {
return `姓名:${user.name},年龄:${user.age}`;
}
// 实际调用时返回 "姓名:undefined,年龄:undefined"
TypeScript 则把错误检查提前到了编译阶段。上面的代码用 TS 写,编辑器会立刻标红:
ts
interface ApiUser {
userName: string;
userAge: number;
}
const user: ApiUser = { userName: "李四", userAge: 25 };
console.log(user.age); // 报错:属性"age"在类型"ApiUser"上不存在
更关键的是,TS 会做类型推断,即使不写类型注解,也能自动分析变量类型。比如:
ts
let message = "hello";
message = 123; // 报错
// 数组类型推断
const numbers = [1, 2, 3];
numbers.push("4"); // 报错
虽然没写 : string 或 : number[],但 TS 能根据初始值推断出准确类型。这种无感化检查既保留了 JS 的灵活,又增加了安全性。
三、语言特性:从「原生语法」到「扩展增强」
TypeScript 作为 JS 的超集,不仅包含所有 JS 语法,还新增了很多实用特性。
比如枚举(Enum),JS 里没有原生枚举,通常用对象模拟:
js
const Status = {
PENDING: 0,
SUCCESS: 1,
ERROR: 2
};
但这样无法限制取值范围,不小心赋值成 3 也不会报错。TS 的枚举则能严格约束:
ts
enum Status {
PENDING,
SUCCESS,
ERROR
}
let status: Status = Status.PENDING;
status = 3; // 报错:不能将类型"3"赋给类型"Status"
枚举还支持字符串值,让代码更具可读性:
ts
enum HttpMethod {
GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE"
}
function request(url: string, method: HttpMethod) {
// ...
}
request("/api/user", HttpMethod.GET);
再比如泛型,能写出适配多种类型的通用代码,同时保持类型安全。JS 实现一个通用的取值函数可能会丢失类型信息:
js
function getValue(obj, key) {
return obj[key];
}
const user = { name: "王五", age: 30 };
const name = getValue(user, "name"); // 类型是 any
用 TS 泛型实现则能保留类型:
ts
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "王五", age: 30 };
const name = getValue(user, "name"); // 类型是 string
const age = getValue(user, "age"); // 类型是 number
TS 还提供了很多内置的泛型工具类型,比如 Partial 可以把对象的所有属性变为可选:
ts
interface User {
name: string;
age: number;
}
type PartialUser = Partial<User>;
// 等价于 { name?: string; age?: number }
此外,TS 支持的接口、抽象类、类型别名等特性,让代码结构更清晰,尤其适合大型项目。比如用抽象类定义一个基础形状类,强制子类实现计算面积的方法:
ts
abstract class Shape {
abstract getArea(): number;
}
class Circle extends Shape {
radius: number;
constructor(radius: number) {
super();
this.radius = radius;
}
getArea() {
return Math.PI * this.radius **2;
}
}
class Square extends Shape {
sideLength: number;
constructor(sideLength: number) {
super();
this.sideLength = sideLength;
}
// 如果忘了实现 getArea 会直接报错
getArea() {
return this.sideLength** 2;
}
}
四、编译过程:从「直接运行」到「转译执行」
JavaScript 是解释型语言,代码可以直接在浏览器或 Node 环境中运行,不需要额外处理。写好的 JS 文件,通过 <script>
标签引入就能执行:
js
<script src="app.js"></script>
TypeScript 则必须经过编译转译,因为浏览器不认识 TS 语法。编译过程会做两件事:一是类型检查,二是把 TS 特有语法转成 JS 代码。比如这段 TS 代码:
ts
enum Direction {
UP,
DOWN
}
function move(dir: Direction) {
// ...
}
编译后会变成纯 JS:
js
var Direction;
(function (Direction) {
Direction[Direction["UP"] = 0] = "UP";
Direction[Direction["DOWN"] = 1] = "DOWN";
})(Direction || (Direction = {}));
function move(dir) {
// ...
}
可以看到,类型注解和枚举语法都被转成了 JS 兼容的代码,类型信息在编译后会被完全移除,不影响运行时性能。
这也意味着 TS 项目需要额外配置编译环境(比如 tsconfig.json),指定编译目标、模块规范等:
json
{
"compilerOptions": {
"target": "ES6", // 编译到 ES6 版本
"module": "ESNext", // 使用 ES 模块系统
"strict": true, // 开启严格模式检查
"outDir": "./dist", // 输出目录
"rootDir": "./src" // 源代码目录
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
在开发过程中,通常会使用 tsc --watch 命令启动监听模式,当 TS 文件变化时自动重新编译,确保始终能运行最新的代码。
五、开发体验:从「全靠记忆」到「智能提示」
写 JavaScript 时,调用函数或访问对象属性全靠记忆或查文档。比如用数组方法时,经常要想 forEach 的参数顺序:
js
[1, 2, 3].forEach((item, index) => {
// 记不清参数是 (index, item) 还是 (item, index)?
});
使用第三方库时更麻烦,比如调用 axios 的 get 方法,得去查文档才知道参数格式和返回值结构:
js
// 不确定第二个参数能不能传配置项
axios.get("/api/data", { timeout: 5000 });
TypeScript 配合现代 IDE(比如 VS Code)能提供精准的智能提示。输入 . 后,编辑器会自动列出可用的属性和方法,还会显示参数类型和返回值:
ts
interface User {
name: string;
age: number;
sayHi: () => void;
}
const user: User = { /* ... */ };
user. // 输入 . 后会提示 name、age、sayHi
引入带有类型定义的库(比如通过 @types 系列包)后,第三方库的方法也能获得完整的类型提示:
ts
import axios from "axios";
// 输入 axios.get 时会提示参数类型和返回值结构
axios.get("/api/data", { timeout: 5000 });
这种提示不仅减少拼写错误,还能帮你快速探索不熟悉的 API。重构代码时更明显,改一个函数的参数类型,所有调用它的地方都会即时报错,不用担心漏改。比如把函数参数从字符串改成数字:
ts
// 原函数
function calculate(num: string) {
// ...
}
calculate("123");
// 修改参数类型后
function calculate(num: number) {
// ...
}
// 调用处会立刻报错,提示需要传入 number 类型
calculate("123");
不是替代,而是增强
很多人觉得 TS 是 JS 的替代品,其实不是。TS 的本质是给 JS 加了一层类型保护,让代码在规模扩大时依然可控。小型项目用 JS 快速迭代没问题,但中大型项目用 TS,长期来看能节省大量调试和维护成本。
这两门语言的关系更像「铅笔与带橡皮的铅笔」------TS 保留了 JS 的灵活,又多了纠错能力。理解它们的核心区别,不是为了争论哪个更好,而是为了在合适的场景用对工具。当项目需要更严格的类型约束和更清晰的代码结构时,TS 无疑是更好的选择。