第9章 9.1 TypeScript 与 Node.js:给 JavaScript 加上「类型保险」
上一章我们学会了用 clinic.js 和 v8-profiler 给 Node.js 应用做「体检」,找出性能瓶颈在哪里。但你有没有这种感觉:代码一长,变量传着传着就不知道是什么类型了,bug 藏在角落里,等你发现的时候已经在线上炸了。这一章我们要聊的,就是怎么让 JavaScript 拥有「类型系统」,在写代码的时候就提前发现低级错误,而不是等到用户报错才后悔。
🎯 开场:为什么 JavaScript 需要类型?
你有没有遇到过这种情况——
// 你的本意是这样
function add(a, b) {
return a + b;
}
add(1, "2"); // 结果是 "12",不是 3!
或者这样——
// 同事改了接口返回格式,你不知道
const user = getUser(); // 突然变成 null 了
console.log(user.name); // 炸了:Cannot read property 'name' of null
这种「写的时候没报错,跑起来才炸」的感觉,就像是不戴头盔骑摩托车——平时没事,出事就是大事。
TypeScript 就是 JavaScript 的「头盔」:它在运行之前就告诉你哪里可能出错。
说白了:TypeScript = JavaScript + 类型检查。它让你在写代码的时候就发现 bug,而不是等到用户打电话投诉。
什么是 TypeScript?
一句话:TypeScript 是 JavaScript 的超集,它给 JavaScript 加上了「类型声明」。
类比一下:
- JavaScript = 裸奔跑步(快是快,但摔了就完蛋)
- TypeScript = 穿好装备再跑(稍微慢一点,但安全得多)
// JavaScript(自由但危险)
function greet(name) {
return "Hello, " + name.toUpperCase();
}
greet(123); // 运行时报错:name.toUpperCase is not a function
// TypeScript(提前告诉你)
function greet(name: string): string {
return "Hello, " + name.toUpperCase();
}
greet(123); // 编辑器直接标红:Argument of type 'number' is not assignable to parameter of type 'string'
看到了吗?TypeScript 在你写代码的瞬间就告诉你错了,不需要等到运行。
学完这章你能解决什么问题?
- ✅ 写出「自解释」的代码,变量类型一目了然
- ✅ 在 VS Code 里实时看到错误提示,不用等跑起来
- ✅ 重构代码时不怕改坏,系统会告诉你哪里受影响
- ✅ 读别人代码像读中文注释一样清楚
🧱 基础:TypeScript 核心概念
1. 类型声明:给变量穿上有标签的衣服
是什么:在变量后面加个 : 类型,告诉 TypeScript 这个变量应该是什么。
生活类比:就像超市货架上的标签——「饮料区」「零食区」「生鲜区」。你不会把洗发水放零食区,因为标签不允许。
怎么用:
// 基本类型声明
let name: string = "小明";
let age: number = 25;
let isStudent: boolean = true;
// 数组类型
let scores: number[] = [98, 87, 92];
let names: Array<string> = ["小红", "小刚", "小丽"];
// 对象类型
let user: { name: string; age: number } = {
name: "小明",
age: 25
};
// 任意类型(尽量少用!)
let anything: any = "可以是任何东西";
anything = 123; // 不报错,因为是 any
解释:第三行 let isStudent: boolean = true;——boolean 就是「真假」类型,只有 true 和 false 两个值。

2. 函数类型:函数的「使用说明书」
是什么:声明函数时写清楚「输入什么类型,输出什么类型」。
为什么要用:团队合作时,你不用看函数内部实现就知道怎么调用它。
怎么用:
// 普通函数
function add(a: number, b: number): number {
return a + b;
}
console.log(add(1, 2)); // 3
console.log(add("1", "2")); // 错误:Argument of type 'string' is not assignable to parameter of type 'number'
// 可选参数(用 ? 表示)
function greet(name: string, greeting?: string): string {
if (greeting) {
return greeting + ", " + name;
}
return "Hello, " + name;
}
console.log(greet("小明")); // Hello, 小明
console.log(greet("小明", "早上好")); // 早上好, 小明
// 默认参数
function say(message: string = "默认值"): string {
return message;
}
console.log(say()); // 默认值
// 箭头函数
const multiply = (a: number, b: number): number => a * b;
console.log(multiply(3, 4)); // 12
解释:第 3 行的 : number 是返回值类型,告诉调用者这个函数会返回一个数字。
3. 接口(Interface):对象的「设计图纸」
是什么:定义一个对象的「形状」,包含哪些属性、什么类型。
生活类比:就像租房合同——规定好了「月租 5000」「押一付三」「禁止养宠物」。不符合合同要求的,房东有权拒绝。
怎么用:
// 定义接口
interface User {
name: string;
age: number;
email?: string; // 可选属性(加 ?)
readonly id: number; // 只读属性(创建后不能改)
}
// 使用接口
function introduce(user: User): string {
return `我是${user.name},今年${user.age}岁`;
}
const xiaoming: User = {
name: "小明",
age: 25,
id: 1001 // 必须提供
};
console.log(introduce(xiaoming)); // 我是小明,今年25岁
// 错误示例:少了必填属性
// const wrong: User = { name: "小明" }; // 报错:Property 'age' is missing
解释:第 3 行的 email?: string 后面有个 ?,表示这个属性可有可无;第 4 行的 readonly id 表示这个属性创建后就不能改。
4. 泛型(Generic):让代码像变形金刚一样灵活
是什么:写代码时不确定具体类型,但要求「前后一致」。
生活类比:就像快递盒子——不管里面装的是手机还是衣服,盒子本身的结构是一样的。泛型就是那个「盒子模板」,你可以塞任何东西进去,但盒子形状不变。
怎么用:
// 不用泛型:写死类型,扩展性差
function firstElement(arr: number[]): number {
return arr[0];
}
function firstElementStr(arr: string[]): string {
return arr[0];
}
// 用泛型:一个函数搞定所有类型
function firstElement<T>(arr: T[]): T {
return arr[0];
}
const nums = [1, 2, 3];
const strs = ["a", "b", "c"];
console.log(firstElement(nums)); // 1(类型是 number)
console.log(firstElement(strs)); // a(类型是 string)
// 泛型接口
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
const userResponse: ApiResponse<User> = {
data: xiaoming,
status: 200,
message: "成功"
};
console.log(userResponse.data.name); // 小明
解释:第 15 行的 <T> 是「类型参数」,调用 firstElement(nums) 时 TypeScript 自动推断 T 是 number,调用 firstElement(strs) 时自动推断 T 是 string。

5. 枚举(Enum):给常量起个好名字
是什么:给一组相关的常量定义语义化的名字。
为什么要用:比直接写数字或字符串更易读,而且改了枚举值,所有用到的地方自动更新。
怎么用:
// 数字枚举
enum Status {
Pending, // 0
Success, // 1
Failed // 2
}
console.log(Status.Pending); // 0
console.log(Status[0]); // Pending(反向映射)
// 字符串枚举(更安全,推荐)
enum OrderStatus {
Created = "CREATED",
Paid = "PAID",
Shipped = "SHIPPED",
Delivered = "DELIVERED"
}
function getStatusText(status: OrderStatus): string {
switch (status) {
case OrderStatus.Created:
return "订单已创建";
case OrderStatus.Paid:
return "已支付";
case OrderStatus.Shipped:
return "已发货";
case OrderStatus.Delivered:
return "已送达";
default:
return "未知状态";
}
}
console.log(getStatusText(OrderStatus.Paid)); // 已支付
解释:第 3-6 行定义了一个枚举 Status,它的值默认是 0、1、2。调用 Status.Pending 就等于用 0,但比直接写 0 易读得多。
6. 联合类型与类型守卫:变量的「多重身份」
是什么:一个变量可以是多种类型之一。
生活类比:就像你的身份——在公司是「员工」,在家里是「家人」,在健身房是「会员」。你一个人有多个身份,TypeScript 里叫「联合类型」。
怎么用:
// 联合类型:一个变量可能是 string 或 number
function printId(id: string | number): void {
console.log("ID:", id);
}
printId(123); // 可以
printId("ABC-001"); // 也可以
// 类型守卫:缩小变量的类型范围
function processValue(value: string | number): string {
if (typeof value === "string") {
// 在这里,TypeScript 知道 value 是 string
return value.toUpperCase();
} else {
// 在这里,TypeScript 知道 value 是 number
return value.toFixed(2);
}
}
console.log(processValue("hello")); // HELLO
console.log(processValue(3.14159)); // 3.14
// 自定义类型守卫
interface Cat {
meow(): void;
}
interface Dog {
bark(): void;
}
function isCat(animal: Cat | Dog): animal is Cat {
return (animal as Cat).meow !== undefined;
}
function makeSound(animal: Cat | Dog): void {
if (isCat(animal)) {
animal.meow();
} else {
animal.bark();
}
}
解释:第 21 行的 typeof value === "string" 叫「类型守卫」——它告诉 TypeScript 在这个分支里 value 是 string 类型,可以安全调用 toUpperCase()。
🔥 实战:3 个递进项目
项目 1(5 分钟):TypeScript 版「成绩统计器」
目标:用 TypeScript 写一个简单的成绩统计函数,体验类型检查的好处。
完整代码:
// score-calculator.ts
interface Student {
name: string;
score: number;
}
// 计算平均分
function calculateAverage(students: Student[]): number {
const total = students.reduce((sum, student) => sum + student.score, 0);
return total / students.length;
}
// 找出最高分和最低分
function findExtremeScores(students: Student[]): { highest: Student; lowest: Student } {
const sorted = [...students].sort((a, b) => b.score - a.score);
return {
highest: sorted[0],
lowest: sorted[sorted.length - 1]
};
}
// 测试数据
const class3Students: Student[] = [
{ name: "小明", score: 92 },
{ name: "小红", score: 88 },
{ name: "小刚", score: 95 },
{ name: "小丽", score: 78 },
{ name: "小华", score: 85 }
];
// 执行统计
const average = calculateAverage(class3Students);
const extremes = findExtremeScores(class3Students);
console.log("=== 成绩统计 ===");
console.log(`平均分:${average.toFixed(1)}`);
console.log(`最高分:${extremes.highest.name} (${extremes.highest.score}分)`);
console.log(`最低分:${extremes.lowest.name} (${extremes.lowest.score}分)`);
预期输出:
=== 成绩统计 ===
平均分:87.6
最高分:小刚 (95分)
最低分:小丽 (78分)
一句话解释:我们定义了 Student 接口作为「设计图纸」,TypeScript 会确保 students 数组里的每个对象都有 name(string)和 score(number)。
项目 2(15 分钟):Node.js 读取 CSV 文件并处理
目标:用 TypeScript 写一个脚本,从 CSV 文件读取销售数据,统计每个月的销售额。
前置准备:先创建一个 sales.csv 文件:
date,product,sales
2024-01-05,手机,5999
2024-01-15,耳机,299
2024-02-01,手机,5999
2024-02-20,充电器,99
2024-03-10,平板,3999
2024-03-25,手机,5999
完整代码:
// sales-analyzer.ts
import * as fs from "fs";
import * as path from "path";
interface SaleRecord {
date: string;
product: string;
sales: number;
}
interface MonthlySales {
month: string;
total: number;
}
// 解析 CSV 文件
function parseCSV(filePath: string): SaleRecord[] {
const content = fs.readFileSync(filePath, "utf-8");
const lines = content.trim().split("\n");
const records: SaleRecord[] = [];
// 跳过表头,从第二行开始
for (let i = 1; i < lines.length; i++) {
const [date, product, salesStr] = lines[i].split(",");
records.push({
date: date.trim(),
product: product.trim(),
sales: parseInt(salesStr.trim(), 10)
});
}
return records;
}
// 按月统计销售额
function summarizeByMonth(records: SaleRecord[]): MonthlySales[] {
const monthlyMap = new Map<string, number>();
for (const record of records) {
const month = record.date.substring(0, 7); // 取 "YYYY-MM" 部分
const current = monthlyMap.get(month) || 0;
monthlyMap.set(month, current + record.sales);
}
// 转换为数组并排序
return Array.from(monthlyMap.entries())
.map(([month, total]) => ({ month, total }))
.sort((a, b) => a.month.localeCompare(b.month));
}
// 主程序
const csvPath = path.join(__dirname, "sales.csv");
const records = parseCSV(csvPath);
const monthlySales = summarizeByMonth(records);
console.log("=== 月度销售统计 ===");
for (const { month, total } of monthlySales) {
console.log(`${month}: ¥${total.toLocaleString()}`);
}
预期输出:
=== 月度销售统计 ===
2024-01: ¥6,298
2024-02: ¥6,098
2024-03: ¥9,998
一句话解释:parseCSV 函数返回 SaleRecord[] 数组,TypeScript 知道每条记录一定有 date(string)、product(string)、sales(number)三个字段。
项目 3(15 分钟):命令行待办清单工具
目标:综合运用 TypeScript 知识,写一个带持久化存储的命令行待办清单。
完整代码:
// todo-cli.ts
import * as fs from "fs";
import * as path from "path";
import * as readline from "readline";
interface Todo {
id: number;
content: string;
completed: boolean;
createdAt: string;
}
enum Priority {
Low = "LOW",
Medium = "MEDIUM",
High = "HIGH"
}
interface TodoWithPriority extends Todo {
priority: Priority;
}
class TodoList {
private todos: TodoWithPriority[] = [];
private readonly filePath: string;
constructor(filePath: string) {
this.filePath = filePath;
this.load();
}
// 从文件加载
private load(): void {
if (fs.existsSync(this.filePath)) {
const data = fs.readFileSync(this.filePath, "utf-8");
this.todos = JSON.parse(data);
}
}
// 保存到文件
private save(): void {
fs.writeFileSync(this.filePath, JSON.stringify(this.todos, null, 2));
}
// 添加待办
add(content: string, priority: Priority = Priority.Medium): void {
const newTodo: TodoWithPriority = {
id: Date.now(),
content,
completed: false,
createdAt: new Date().toISOString(),
priority
};
this.todos.push(newTodo);
this.save();
console.log(`✅ 已添加:[${priority}] ${content}`);
}
// 标记完成
complete(id: number): void {
const todo = this.todos.find(t => t.id === id);
if (!todo) {
console.log(`❌ 未找到 ID 为 ${id} 的待办`);
return;
}
todo.completed = true;
this.save();
console.log(`✅ 已完成:${todo.content}`);
}
// 列出所有
list(filter?: "all" | "active" | "completed"): void {
const filtered = this.todos.filter(t => {
if (filter === "active") return !t.completed;
if (filter === "completed") return t.completed;
return true;
});
if (filtered.length === 0) {
console.log("📋 没有待办事项");
return;
}
console.log("\n📋 待办清单:");
for (const todo of filtered) {
const status = todo.completed ? "✅" : "⬜";
const priorityEmoji =
todo.priority === Priority.High ? "🔴" :
todo.priority === Priority.Medium ? "🟡" : "🟢";
console.log(`${status} [${todo.id}] ${priorityEmoji} ${todo.content}`);
}
console.log();
}
// 删除
delete(id: number): void {
const index = this.todos.findIndex(t => t.id === id);
if (index === -1) {
console.log(`❌ 未找到 ID 为 ${id} 的待办`);
return;
}
const [removed] = this.todos.splice(index, 1);
this.save();
console.log(`🗑️ 已删除:${removed.content}`);
}
}
// 使用示例
const todoFile = path.join(__dirname, "todos.json");
const todoList = new TodoList(todoFile);
// 命令行测试
console.log("=== 待办清单工具 ===\n");
todoList.add("完成 TypeScript 作业", Priority.High);
todoList.add("阅读 Node.js 文档", Priority.Medium);
todoList.add("整理笔记", Priority.Low);
todoList.list();
todoList.complete(Number(process.argv[2] || "0"));
todoList.list("active");
预期输出:
=== 待办清单工具 ===
✅ 已添加:[HIGH] 完成 TypeScript 作业
✅ 已添加:[MEDIUM] 阅读 Node.js 文档
✅ 已添加:[LOW] 整理笔记
📋 待办清单:
⬜ [1734567890001] 🔴 完成 TypeScript 作业
⬜ [1734567890002] 🟡 阅读 Node.js 文档
⬜ [1734567890003] 🟢 整理笔记
✅ 已完成:完成 TypeScript 作业
📋 待办清单:
⬜ [1734567890002] 🟡 阅读 Node.js 文档
⬜ [1734567890003] 🟢 整理笔记
一句话解释:我们用 TodoList 类封装了所有操作,用 save() 和 load() 方法实现数据持久化,TypeScript 的类型系统确保了「每条待办一定有 id、content、completed 这些字段」。
💪 进阶:常见坑 + 调试技巧
坑 1:类型断言别滥用
// ❌ 错误:强行告诉 TypeScript "这是 string",但实际上是 number
const value: any = 123;
const str: string = value as string;
console.log(str.toUpperCase()); // 运行时才报错!
// ✅ 正确:先用类型守卫判断
const value2: string | number = 123;
if (typeof value2 === "string") {
console.log(value2.toUpperCase());
} else {
console.log(value2.toFixed(2));
}
坑 2:对象属性别漏掉
// ❌ 错误:接口定义的属性必须全部提供
interface Config {
host: string;
port: number;
}
const config: Config = { host: "localhost" }; // 报错:Property 'port' is missing
// ✅ 正确:要么补全,要么标记为可选
interface Config2 {
host: string;
port?: number; // 加 ? 表示可选
}
const config2: Config2 = { host: "localhost" }; // 现在 OK 了
坑 3:数组类型写反了
// ❌ 错误:写成 (string),意思是一个可以装任何东西的数组
const arr1: string = [1, 2, 3]; // 报错!
// ✅ 正确:number[] 表示「number 类型的数组」
const arr2: number[] = [1, 2, 3];
// 或者用泛型写法
const arr3: Array<number> = [1, 2, 3];
坑 4:null 和 undefined 是独立的类型
// ❌ 错误:以为 undefined 可以赋值给 string
let name: string = undefined; // 报错!
// ✅ 正确:需要明确声明
let name2: string | undefined = undefined; // 现在 OK 了
// 或者用非空断言(确定有值的时候)
let name3: string = "小明";
console.log(name3!.toUpperCase()); // ! 告诉 TypeScript 我确定不是 null/undefined
坑 5:箭头函数的返回值别忘了标
// ❌ 错误:忘记标注返回类型,TypeScript 推断为 void
const double = (n: number) => {
n * 2; // 少了 return!这只是个表达式,不会返回任何东西
};
console.log(double(5)); // undefined
// ✅ 正确:加上返回类型
const double2 = (n: number): number => {
return n * 2;
};
// 或者更简洁
const double3 = (n: number): number => n * 2;
调试技巧:用 console.log + 类型注解
// 遇到类型问题时,先加个 console.log 看看实际值
function processData(data: unknown) {
console.log("收到的数据:", data);
console.log("数据类型:", typeof data);
// 加上类型注解,帮助自己理清思路
if (typeof data === "object" && data !== null) {
console.log("是个对象");
}
}
processData({ name: "小明", age: 25 });
processData([1, 2, 3]);
processData("hello");
✏️ 练习题
练习 1(2 分钟):基础改写
- 输入:把项目 1 的
class3Students改成你自己的 3 个朋友的名字和成绩 - 预期输出:
console.log打印出正确的平均分和最高/最低分 - 提示:直接改
students数组里的值就行
练习 2(2 分钟):加个判断
- 输入:在项目 1 的
calculateAverage函数里加一个判断,如果数组为空就返回0 - 预期输出:
calculateAverage([])返回0,calculateAverage(class3Students)返回87.6 - 提示:在函数开头加
if (students.length === 0) return 0;
练习 3(3 分钟):处理新数据
- 输入:把项目 2 的 CSV 解析改成处理这个数据:
date,product,quantity
2024-01-05,可乐,10
2024-01-15,薯片,5
2024-02-01,可乐,8
- 预期输出:输出每月销售总数(数量,不是金额)
- 提示:把
sales字段改成quantity就行
练习 4(3 分钟):串联两个项目
- 输入:用项目 2 的 CSV 读取方式,读取一个包含
name和score的 CSV,然后用项目 1 的统计函数处理 - 预期输出:打印出平均分和最高/最低分
- 提示:关键是让 CSV 的列名和
Student接口的字段名对上
练习 5(5 分钟):分析报错
- 输入:看下面这段代码,说出为什么报错,然后修复它
interface User {
name: string;
age: number;
}
const users: User[] = [
{ name: "小明", age: 25 },
{ name: "小红", age: "二十岁" }, // 这行报错
{ name: "小刚", age: 30 }
];
- 预期输出:说出错误原因,并写出修复后的代码
- 提示:
"二十岁"是字符串,不是数字
作业:做一个「TypeScript 学生信息管理系统」
需求描述:用 TypeScript 写一个学生信息管理系统,可以从 JSON 文件读取学生数据,提供查询、添加、统计功能。
功能点:
1. 从 students.json 文件读取学生列表
2. 支持按姓名查询学生信息
3. 统计全班平均年龄和男生/女生比例
加分项:
1. 支持按年龄排序输出
2. 把结果保存到新文件
验收标准:
- 能成功读取 students.json 并显示所有学生
- 输入姓名能正确返回学生信息(或「未找到」)
- 平均年龄和性别比例计算正确
- 代码有适当注释
students.json 示例:
[
{ "name": "小明", "age": 15, "gender": "male", "score": 92 },
{ "name": "小红", "age": 14, "gender": "female", "score": 88 },
{ "name": "小刚", "age": 15, "gender": "male", "score": 95 }
]
📚 总结
本文学了 3 件事:
- TypeScript 的「类型声明」让你在写代码时就发现 bug
- interface 和 泛型 让代码像设计图纸一样清晰、可复用
- Node.js 配合 TypeScript 让你写出更健壮的服务器端代码
延伸资源:
- TypeScript 官方文档(英文,但例子很清楚)
- 《TypeScript 入门教程》- 阮一峰(中文,适合零基础)
- TypeScript Playground(在线写 TS,无需安装)
互动钩子:你在写 JavaScript 的时候有没有遇到过「类型相关」的 bug?比如把数字和字符串混在一起导致计算出错?评论区聊聊,老粉优先回复!
📌 下章预告:写好了 TypeScript 代码,怎么确保它真的能跑?下一章我们来聊聊「测试」——用 Jest 和 Supertest 给你的代码装上「安全气囊」,每次修改都不怕改坏。

评论(0)