第8章 8.1 TypeScript 入门
🎯 为什么要学 TypeScript?
上一章我们手把手写了一个迷你 Vue 响应式数据绑定框架,是不是感觉 JavaScript 终于「听话」了一点?
但你有没有遇到过这种崩溃时刻:
- 辛辛苦苦写的函数,别人调用时传错了参数类型,bug 找半天
- 改了某个变量名,结果别的地方引用没改,线上报错了
- 看别人写的代码,想知道这个变量到底是字符串还是数组,只能靠猜
JavaScript 是一门「事后报错」的语言——错了就错了,运行了才知道。Python 虽然也是动态类型,但有 type hint 可以标注,而 JavaScript 以前啥都没有。
TypeScript 就是来解决这个问题的。它是 JavaScript 的「超集」,给你的代码加上编译时的类型检查,让你在写代码的时候就能发现错误,而不是等到用户报错。
类比一下:JavaScript 就像外卖点餐,点了才知道送来的对不对;TypeScript 就像在餐厅看菜单点菜,厨房按你的单子做,出错概率低多了。
学完这章,\n\n
\n\n
\n\n你将能够:
- 搭建 TypeScript 开发环境
- 写出有类型标注的 JavaScript 代码
- 用 Interface 定义复杂数据结构
- 独立完成一个小工具的 TypeScript 版本
🧱 基础 25 分钟:核心概念
1. 环境准备:安装 TypeScript
TypeScript 需要先安装(用 npm),然后你写的 .ts 文件要编译成 .js 才能运行。
# 全局安装 TypeScript 编译器
npm install -g typescript
# 验证安装成功
tsc --version
安装完成后,你就有了一个叫 tsc 的命令,它能把 .ts 文件编译成普通的 .js 文件。
2. 配置文件 tsconfig.json
每个 TypeScript 项目最好有一个 tsconfig.json,告诉编译器怎么工作。
# 在项目文件夹里生成配置文件
tsc --init
生成的默认配置大概长这样:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"outDir": "./dist"
},
"include": ["src/**/*"]
}
解释一下几个关键配置:
- target:编译成哪个版本的 JavaScript(ES2020 比较现代,兼容性也不错)
- outDir:编译输出到哪里
- strict:开启严格模式,建议打开,能帮你 catch 更多错误
3. 基础类型注解
这是 TypeScript 的核心——给变量加「类型标签」。
# Python 版本的类型提示
name: str = "小明"
age: int = 25
is_student: bool = True
TypeScript 写法几乎一样:
// TypeScript 基础类型注解
let name: string = "小明";
let age: number = 25;
let isStudent: boolean = true;
// 没有初始值的变量也要标类型
let score: number;
let title: string;
// 数组类型:两种写法都行
let fruits: string[] = ["苹果", "香蕉"];
let numbers: Array<number> = [1, 2, 3];
类比一下:类型注解就像给超市商品贴标签,「这是水果区」「这是饮料区」,TypeScript 编译器看到标签就知道你放的东西对不对。
4. Interface(接口):定义复杂数据结构
这是 TypeScript 最强大的武器——用 interface 定义一个「数据结构蓝图」。
// 定义一个「用户」的数据结构
interface User {
id: number;
name: string;
email: string;
age?: number; // 问号表示这个字段可选
}
// 用这个蓝图创建数据
let xiaoming: User = {
id: 1,
name: "小明",
email: "xiaoming@example.com"
};
// 少了必填字段会报错
// let wrong: User = { name: "小红" }; // 错误!缺少 id 和 email
// 多加了字段也会报错
// let wrong2: User = { id: 2, name: "小红", email: "xiao@hong.com", extra: "值钱" }; // 错误!
接口就像填表时的「必填项」——你告诉 TypeScript:创建用户时,id、name、email 必须有,age 可以没有。
5. 函数类型注解
函数的输入输出也要标类型:
// 普通函数:参数和返回值都要标
function greet(name: string, times: number): string {
let result = "";
for (let i = 0; i < times; i++) {
result += `你好,${name}!`;
}
return result;
}
console.log(greet("小明", 2));
// 输出:你好,小明!你好,小明!
// 箭头函数也一样
const add = (a: number, b: number): number => a + b;
console.log(add(3, 5));
// 输出:8
6. 联合类型与类型别名
有时候一个变量可以是多种类型,用 | 分隔:
// 联合类型:可以是字符串或数字
let identifier: string | number = "ABC123";
identifier = 10086; // 也可以是数字,没问题
// 类型别名:给复杂类型起个短名字
type Status = "pending" | "success" | "error";
type ID = string | number;
let orderStatus: Status = "pending";
let userId: ID = 12345;
7. 编译运行
写完 .ts 文件,用 tsc 编译,然后运行:
# 编译 src 目录下的所有 .ts 文件
tsc
# 或者指定文件
tsc src/index.ts
# 编译 + 运行
tsc src/index.ts && node src/index.js
🔥 实战 35 分钟:3 个递进小项目
项目 1:用户信息管理(5 分钟)
目标:理解 TypeScript 类型注解和 Interface 的基本用法。
创建 src/project1.ts:
interface User {
id: number;
name: string;
email: string;
age?: number;
}
// 创建用户
function createUser(name: string, email: string, age?: number): User {
return {
id: Math.floor(Math.random() * 10000), // 随机生成 ID
name,
email,
age
};
}
// 显示用户信息
function showUser(user: User): void {
console.log(`【用户信息】`);
console.log(` ID: ${user.id}`);
console.log(` 姓名: ${user.name}`);
console.log(` 邮箱: ${user.email}`);
if (user.age !== undefined) {
console.log(` 年龄: ${user.age}`);
}
}
// 测试
const user1 = createUser("小明", "xiaoming@example.com", 25);
const user2 = createUser("小红", "xiaohong@example.com");
showUser(user1);
showUser(user2);
预期输出:
【用户信息】
ID: 7429
姓名: 小明
邮箱: xiaoming@example.com
年龄: 25
【用户信息】
ID: 3182
姓名: 小红
邮箱: xiaohong@example.com
解释:定义了 User 接口后,TypeScript 知道 createUser 必须返回有 id、name、email 的对象,showUser 也只接受符合这个结构的数据。
项目 2:任务清单管理(15 分钟)
目标:从 JSON 文件读取数据,用 Interface 描述任务结构,实现增删改查。
首先创建 data/tasks.json:
[
{ "id": 1, "title": "买菜", "completed": false, "priority": "high" },
{ "id": 2, "title": "写周报", "completed": true, "priority": "medium" },
{ "id": 3, "title": "健身", "completed": false, "priority": "low" }
]
然后创建 src/project2.ts:
import * as fs from "fs";
// 定义任务数据结构
interface Task {
id: number;
title: string;
completed: boolean;
priority: "high" | "medium" | "low"; // 限定只能是这三个值
}
// 从 JSON 读取任务
function loadTasks(filename: string): Task[] {
const data = fs.readFileSync(filename, "utf-8");
return JSON.parse(data) as Task[];
}
// 显示所有任务
function showTasks(tasks: Task[]): void {
console.log("\n📋 任务清单:");
console.log("─".repeat(40));
tasks.forEach(task => {
const status = task.completed ? "✅" : "⬜";
const priorityIcon = task.priority === "high" ? "🔴" : task.priority === "medium" ? "🟡" : "🟢";
console.log(`${status} [${task.id}] ${task.title} ${priorityIcon}`);
});
console.log("─".repeat(40));
const done = tasks.filter(t => t.completed).length;
console.log(`完成进度:${done}/${tasks.length}`);
}
// 添加新任务
function addTask(tasks: Task[], title: string, priority: "high" | "medium" | "low"): Task[] {
const maxId = tasks.reduce((max, t) => Math.max(max, t.id), 0);
const newTask: Task = {
id: maxId + 1,
title,
completed: false,
priority
};
return [...tasks, newTask];
}
// 切换任务状态
function toggleTask(tasks: Task[], id: number): Task[] {
return tasks.map(task =>
task.id === id ? { ...task, completed: !task.completed } : task
);
}
// 主程序
const tasks = loadTasks("data/tasks.json");
showTasks(tasks);
// 添加任务
const tasks2 = addTask(tasks, "学 TypeScript", "high");
showTasks(tasks2);
// 切换状态
const tasks3 = toggleTask(tasks2, 1);
showTasks(tasks3);
预期输出:
📋 任务清单:
────────────────────────────────────────
⬜ [1] 买菜 🔴
✅ [2] 写周报 🟡
⬜ [3] 健身 🟢
────────────────────────────────────────
完成进度:1/3
📋 任务清单:
────────────────────────────────────────
⬜ [1] 买菜 🔴
✅ [2] 写周报 🟡
⬜ [3] 健身 🟢
⬜ [4] 学 TypeScript 🔴
────────────────────────────────────────
完成进度:1/4
📋 任务清单:
────────────────────────────────────────
✅ [1] 买菜 🔴
✅ [2] 写周报 🟡
⬜ [3] 健身 🟢
⬜ [4] 学 TypeScript 🔴
────────────────────────────────────────
完成进度:2/4
解释:Interface 让数据结构一目了然,TypeScript 会检查你是否有拼写错误或漏掉字段。
项目 3:个人记账本小工具(15 分钟)
目标:综合运用 Interface、函数类型、联合类型,写一个有点真实用途的记账工具。
创建 src/project3.ts:
import * as fs from "fs";
// ========== 类型定义 ==========
type TransactionType = "income" | "expense";
interface Transaction {
id: number;
type: TransactionType;
amount: number;
category: string;
note: string;
date: string;
}
interface AccountBook {
transactions: Transaction[];
balance: number;
}
// ========== 工具函数 ==========
// 生成格式化日期
function getToday(): string {
return new Date().toISOString().split("T")[0];
}
// 格式化金额(保留2位小数,加千分位)
function formatMoney(amount: number): string {
return "¥" + amount.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
// ========== 核心功能 ==========
// 创建账本
function createAccountBook(): AccountBook {
return {
transactions: [],
balance: 0
};
}
// 加载账本(如果有数据的话)
function loadAccountBook(filename: string): AccountBook {
if (!fs.existsSync(filename)) {
return createAccountBook();
}
const data = fs.readFileSync(filename, "utf-8");
return JSON.parse(data);
}
// 保存账本
function saveAccountBook(book: AccountBook, filename: string): void {
fs.writeFileSync(filename, JSON.stringify(book, null, 2));
console.log("💾 已保存到", filename);
}
// 记一笔账
function addTransaction(
book: AccountBook,
type: TransactionType,
amount: number,
category: string,
note: string
): AccountBook {
if (amount <= 0) {
console.error("❌ 金额必须大于 0");
return book;
}
const maxId = book.transactions.length > 0
? Math.max(...book.transactions.map(t => t.id))
: 0;
const transaction: Transaction = {
id: maxId + 1,
type,
amount,
category,
note,
date: getToday()
};
const newTransactions = [...book.transactions, transaction];
const balanceChange = type === "income" ? amount : -amount;
return {
transactions: newTransactions,
balance: book.balance + balanceChange
};
}
// 显示账本摘要
function showSummary(book: AccountBook): void {
console.log("\n💰 个人记账本");
console.log("═".repeat(40));
console.log(`💵 当前余额:${formatMoney(book.balance)}`);
console.log(`📝 记账笔数:${book.transactions.length}`);
console.log("─".repeat(40));
if (book.transactions.length > 0) {
console.log("最近 5 笔记录:");
const recent = book.transactions.slice(-5).reverse();
recent.forEach(t => {
const icon = t.type === "income" ? "📈" : "📉";
const sign = t.type === "income" ? "+" : "-";
console.log(` ${icon} ${t.date} ${t.category} ${sign}${formatMoney(t.amount)}`);
});
}
console.log("═".repeat(40));
}
// 显示月度统计
function showMonthlyStats(book: AccountBook, month?: string): void {
const targetMonth = month || getToday().substring(0, 7); // 默认本月
const monthly = book.transactions.filter(t => t.date.startsWith(targetMonth));
if (monthly.length === 0) {
console.log(`\n📭 ${targetMonth} 暂无记录`);
return;
}
const income = monthly.filter(t => t.type === "income").reduce((sum, t) => sum + t.amount, 0);
const expense = monthly.filter(t => t.type === "expense").reduce((sum, t) => sum + t.amount, 0);
console.log(`\n📊 ${targetMonth} 月度统计`);
console.log("─".repeat(30));
console.log(` 📈 收入:${formatMoney(income)}`);
console.log(` 📉 支出:${formatMoney(expense)}`);
console.log(` 💡 结余:${formatMoney(income - expense)}`);
}
// ========== 主程序 ==========
// 初始化或加载账本
const bookFile = "data/account_book.json";
let myBook = loadAccountBook(bookFile);
console.log("👋 欢迎使用个人记账本!");
// 模拟记账操作
myBook = addTransaction(myBook, "income", 15000, "工资", "6月份工资");
myBook = addTransaction(myBook, "expense", 3000, "房租", "6月份房租");
myBook = addTransaction(myBook, "expense", 150, "餐饮", "午餐");
myBook = addTransaction(myBook, "expense", 80, "交通", "地铁月卡");
myBook = addTransaction(myBook, "income", 200, "副业", "帮人修电脑");
showSummary(myBook);
showMonthlyStats(myBook);
// 保存
saveAccountBook(myBook, bookFile);
预期输出:
👋 欢迎使用个人记账本!
💰 个人记账本
════════════════════════════════════
💵 当前余额:¥11,970.00
📝 记账笔数:5
────────────────────────────────────
最近 5 笔记录:
📈 2026-06-26 副业 +¥200.00
📉 2026-06-26 交通 -¥80.00
📉 2026-06-26 餐饮 -¥150.00
📉 2026-06-26 房租 -¥3,000.00
📈 2026-06-26 工资 +¥15,000.00
════════════════════════════════════
📊 2026-06 月度统计
──────────────────────────────
📈 收入:¥15,200.00
📉 支出:¥3,230.00
💡 结余:¥11,970.00
💾 已保存到 data/account_book.json
解释:这个项目展示了 Interface 如何让复杂数据结构清晰可控,TypeScript 的类型检查在每次添加账目时都在保护你不出错。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:类型推断不是万能的
❌ 错误示例:
let data = { x: 1, y: "hello" };
data.z = 100; // ❌ 错误!data 被推断为 { x: number; y: string },没有 z
✅ 正确做法:明确标注类型或者用扩展语法
let data: { x: number; y: string; z?: number } = { x: 1, y: "hello" };
data.z = 100; // ✅ OK
坑 2:接口与类型别名的区别
// interface 可以被 extends 扩展,可以声明合并
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// type 可以做联合类型、交叉类型
type ID = string | number;
type Point = { x: number } & { y: number };
一般优先用 interface,需要联合类型时用 type。
坑 3:函数返回 void 不是「不返回值」
function logSomething(msg: string): void {
console.log(msg);
return undefined; // ✅ void 允许返回 undefined
// return null; // ❌ void 不允许返回 null
}
坑 4:strict 模式下 null 要小心
let name: string = null; // ❌ strict 模式下报错
let name: string | null = null; // ✅ 显式声明可以是 null
坑 5:不要用 any 逃过类型检查
❌ 这样等于没用 TypeScript:
function processData(data: any) {
return data.x + data.y; // 完全没有类型安全
}
✅ 正确做法:定义接口或用 unknown
interface Data {
x: number;
y: number;
}
function processData(data: Data) {
return data.x + data.y; // ✅ 有类型检查
}
调试技巧:VS Code + TypeScript
TypeScript 最方便的调试是IDE 支持。在 VS Code 中:
- 鼠标悬停在变量上,看类型推断
- 用 Cmd + Click(Mac)或 Ctrl + Click(Windows)跳转到定义
- 安装 ESLint 插件,实时检查类型错误
# 或者用 tsc 的 watch 模式,文件保存自动编译
tsc --watch
✏️ 练习题
练习 1(2 分钟):类型注解改改看
- 输入:把下面的代码改用
interface定义Product,并给变量加上类型注解
let name = "手机";
let price = 2999;
let inStock = true;
- 预期输出:能编译通过,无报错
- 提示:定义
interface Product { name: string; price: number; inStock: boolean; }
练习 2(2 分钟):让 Interface 更严格
- 输入:给下面的
User接口加上email必填、phone可选
interface User {
id: number;
name: string;
}
- 预期输出:
// 这段代码应该报错(email 缺失)
// { id: 1, name: "小明" }
// 这段应该通过
// { id: 2, name: "小红", email: "xiao@hong.com", phone: "13800001111" }
- 提示:在 Interface 里加
email: string;和phone?: string;
练习 3(3 分钟):添加一个数组过滤函数
- 输入:基于项目 2 的
Task接口,写一个函数filterByPriority,输入任务数组和优先级,返回符合的任务 - 预期输出:
const result = filterByPriority(tasks, "high");
// result 只包含 priority 为 "high" 的任务
- 提示:用
.filter(t => t.priority === priority)
练习 4(5 分钟):串联项目 1 和项目 3
- 输入:给项目 3 的
addTransaction函数加上「事务类型」验证,如果不是 "income" 或 "expense" 就报错 - 预期输出:传入非法类型时控制台输出错误信息
- 提示:用
if (type !== "income" && type !== "expense")
练习 5(5 分钟):修复类型错误
- 输入:下面这段代码有 3 处类型错误,请找出并修复
interface Config {
host: string;
port: number;
ssl: boolean;
}
const config: Config = {
host: "localhost",
port: "8080", // 错误1
ssl: "true" // 错误2
};
function connect(cfg: Config): void {
console.log(`连接 ${cfg.host}:${cfg.port}`);
}
connect({
host: 123, // 错误3
port: 3000
});
- 预期输出:编译无错误
- 提示:检查
port应该是数字、ssl应该是布尔、host应该是字符串
作业:做一个「TypeScript 版本的通讯录管理工具」
需求描述:做一个命令行通讯录,可以用它存储、查找、删除联系人。
功能点:
1. 添加联系人(姓名、电话、邮箱、分类:朋友/同事/家人)
2. 列出所有联系人
3. 按姓名搜索联系人
4. 删除联系人
5. 数据保存到 JSON 文件,程序重启后数据不丢失
加分项:
1. 支持按分类筛选(只显示朋友或同事)
2. 添加「生日」字段,并能在联系人列表中显示是否快到了
验收标准:
- 能编译运行(tsc && node dist/contacts.js)
- 能增删改查,有持久化
- 代码有适当注释
- 使用 Interface 定义数据结构
提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
本文学了 3 件事:
1. TypeScript 环境搭建和编译流程
2. 类型注解和 Interface 让代码结构清晰可控
3. 用 TypeScript 写了一个能跑的任务清单 + 记账本
延伸学习:
- TypeScript 官方文档(中文版也有)
- 《Programming TypeScript》—— 进阶必读,讲得很透
- TypeScript Deep Dive(免费在线书)
互动钩子:
你有没有在项目中遇到过「类型导致的 bug」?比如 JavaScript 传错参数类型导致线上报错?当时是怎么发现的?评论区聊聊,老粉优先回复!
下一章我们要学的是泛型与高级类型,那是 TypeScript 最强大的部分之一。想象一下:如果有一个「万能容器」可以装任何类型的数据,但又不丢失类型信息——那会怎么用?剧透一点点:泛型就是给你的代码装上「智能眼镜」,让 TypeScript 知道这个容器里装的到底是什么。

评论(0)