第6章 6.1 Python 实战:用 TypeScript 的思路写 Type-Safe 代码
上一章我们学会了自定义 Hooks(composables),把逻辑拆成可复用的小块。
这一章,我们回到 TypeScript + Vue3 这个主题。很多人学 Vue3 时被 TypeScript 劝退,觉得类型系统太麻烦。但你有没有想过:TypeScript 其实是来帮你偷懒的?
学完这章,你将能:
- 理解 TypeScript 是什么(说白了就是给 JavaScript 加了个「纠错助手」)
- 写出带类型的 Vue3 组件,不再被 undefined is not an object 折磨
- 用泛型写出会「自我解释」的代码
🎯 开场 3 分钟:为什么你的代码总在报错?
想象一个场景:
你写了一个 Vue 组件,3 个月后重构,发现有个变量 user 在某些地方是对象,某些地方是 null,还有些地方变成了 undefined。
你疯狂 console.log 找 bug,最后\n\n
\n\n
\n\n发现是接口返回数据不一致导致的。
这就是没有类型的痛苦。
TypeScript 就是来解决这个问题的——它在代码跑之前就告诉你:「喂,这个地方可能是 null,你没检查就用了!」
类比:TypeScript 就像超市的自动门传感器,你还没撞上去,它就提醒你「门关着呢」。
🧱 基础 25 分钟:TypeScript 核心概念
什么是 TypeScript?
TypeScript = JavaScript + 类型检查。
JavaScript 是「先跑跑看」的语言,TypeScript 是「跑之前先检查一遍」的语言。
# 等等!这是 Python 教程
# 但 TypeScript 的思路可以用 Python 模拟
# 下面我们用 Python 的 type hints 来类比 TypeScript 的用法
Python 的 type hints:TypeScript 的孪生兄弟
TypeScript 用 let name: string = "小明",Python 用:
name: str = "小明"
age: int = 25
is_active: bool = True
这就是 Python 的类型注解,作用和 TypeScript 一样:让变量类型一目了然。
为什么要用类型?
痛点:你想知道一个函数返回什么,得翻半天源码。
解决:类型注解就是函数的「说明书」。
def get_user(id: int) -> dict[str, str]:
"""根据 ID 获取用户信息"""
return {"name": "小明", "email": "xiaoming@example.com"}
# 任何人看到这个签名都知道:
# 输入:int 类型
# 输出:dict[str, str] 类型
ref:Vue3 的响应式引用
在 Vue3 + TypeScript 里,你这样写:
// TypeScript
const count = ref<number>(0)
const user = ref<User | null>(null)
用 Python 类比,就是加了一层「响应式包装」:
from dataclasses import dataclass
from typing import Optional
@dataclass
class User:
name: str
email: str
# 普通变量(不是响应式的,但类型明确)
user: Optional[User] = None
count: int = 0
defineProps:组件的属性签名
Vue3 的 props 定义:
// TypeScript 版本
interface Props {
title: string
count?: number // 可选
onClick: () => void
}
const props = defineProps<Props>()
Python 版本(用 dataclass 模拟):
from dataclasses import dataclass, field
@dataclass
class Props:
title: str
count: int = 0 # 有默认值 = 可选
on_click: callable = field(default=lambda: None) # 回调
# 使用
props = Props(title="点击我", count=5)
泛型:会「自我变形」的函数
泛型就是「参数化的类型」——函数能接受任何类型,但返回同类型的值。
from typing import TypeVar
T = TypeVar('T')
def first_element(items: list[T]) -> T:
"""返回列表的第一个元素,类型保持不变"""
if not items:
raise ValueError("列表不能为空")
return items[0]
# 用字符串列表调用
names = ["Alice", "Bob", "Charlie"]
first = first_element(names) # first 的类型是 str
# 用数字列表调用
scores = [95, 82, 78]
top = first_element(scores) # top 的类型是 int
类比:泛型就像「通用快递箱」——你放什么进去,它就还是什么出来,只是箱子规格不变。
defineEmits:带类型的 emits
// TypeScript
const emit = defineEmits<{
(e: 'update', value: string): void
(e: 'delete', id: number): void
}>()
emit('update', 'new value')
Python 版本(用协议模拟):
from typing import Protocol, Callable
class Emits(Protocol):
def on_update(self, value: str) -> None: ...
def on_delete(self, id: int) -> None: ...
# 模拟 Vue 的 emit 模式
class ComponentEmits:
def __init__(self):
self._handlers: dict[str, list[Callable]] = {}
def emit(self, event: str, *args) -> None:
for handler in self._handlers.get(event, []):
handler(*args)
def on(self, event: str, handler: Callable) -> None:
if event not in self._handlers:
self._handlers[event] = []
self._handlers[event].append(handler)
# 使用
emits = ComponentEmits()
emits.on('update', lambda value: print(f"更新了: {value}"))
emits.emit('update', 'new value') # 输出: 更新了: new value
🔥 实战 35 分钟:3 个递进项目
项目 1(5 分钟):类型化的待办清单
目标:学会用 dataclass + Optional 定义带类型的 Todo 项。
from dataclasses import dataclass
from typing import Optional
from datetime import datetime
@dataclass
class Todo:
id: int
title: str
completed: bool = False
created_at: str = ""
def __post_init__(self):
if not self.created_at:
self.created_at = datetime.now().isoformat()
def mark_done(self) -> None:
self.completed = True
def __repr__(self) -> str:
status = "✅" if self.completed else "⬜"
return f"{status} [{self.id}] {self.title}"
# 创建几个待办
todos: list[Todo] = [
Todo(1, "写周报"),
Todo(2, "回复邮件"),
Todo(3, "开会"),
]
# 标记完成
todos[0].mark_done()
# 打印所有待办
for todo in todos:
print(todo)
预期输出:
✅ [1] 写周报
⬜ [2] 回复邮件
⬜ [3] 开会
解释:每个 Todo 对象都有明确的类型,IDE 会提示你可用的方法。
项目 2(15 分钟):从 JSON 文件读取并过滤数据
目标:读取包含多个用户任务的 JSON,用类型安全的过滤。
import json
from dataclasses import dataclass, field
from typing import Optional
# 模拟从文件读取的 JSON 数据
json_data = '''
[
{"id": 1, "name": "小明", "tasks": ["写代码", "开会", "Code Review"]},
{"id": 2, "name": "小红", "tasks": ["写文档", null, "上线部署"]},
{"id": 3, "name": "小刚", "tasks": ["设计图", "开发", "测试"]},
{"id": 4, "name": "小丽", "tasks": []}
]
'''
@dataclass
class User:
id: int
name: str
tasks: list[Optional[str]]
@property
def active_tasks(self) -> list[str]:
"""返回非空任务"""
return [t for t in self.tasks if t is not None]
@property
def task_count(self) -> int:
return len(self.active_tasks)
def load_users(json_str: str) -> list[User]:
"""解析 JSON 并返回类型化的用户列表"""
raw_data = json.loads(json_str)
return [User(**user_data) for user_data in raw_data]
def filter_users_by_task(users: list[User], keyword: str) -> list[User]:
"""筛选出包含特定任务的用户"""
return [
user for user in users
if any(keyword.lower() in task.lower() for task in user.active_tasks)
]
# 主流程
users = load_users(json_data)
print("=== 所有用户任务统计 ===")
for user in users:
print(f"{user.name}: {user.task_count} 个任务")
for task in user.active_tasks:
print(f" - {task}")
print("\n=== 包含「开发」的用户 ===")
dev_users = filter_users_by_task(users, "开发")
for user in dev_users:
print(f"{user.name} 参与开发")
预期输出:
=== 所有用户任务统计 ===
小明: 3 个任务
- 写代码
- 开会
- Code Review
小红: 2 个任务
- 写文档
- 上线部署
小刚: 3 个任务
- 设计图
- 开发
- 测试
小丽: 0 个任务
=== 包含「开发」的用户 ===
小刚 参与开发
解释:即使 JSON 里有 null,Optional[str] 类型也能安全处理,不会报错。
项目 3(15 分钟):组合做个「任务提醒小工具」
目标:综合前两个项目,写一个带统计和筛选的小工具。
from dataclasses import dataclass, field
from typing import Optional
from datetime import datetime, timedelta
import random
@dataclass
class Task:
id: int
title: str
priority: str # "high", "medium", "low"
due_days: int # 距离截止还有几天
completed: bool = False
@property
def is_overdue(self) -> bool:
return self.due_days < 0 and not self.completed
@property
def urgency_label(self) -> str:
if self.completed:
return "已完成"
if self.due_days < 0:
return f"已逾期 {-self.due_days} 天"
if self.due_days == 0:
return "今天到期!"
if self.due_days <= 2:
return f"还剩 {self.due_days} 天"
return "进行中"
@dataclass
class TaskManager:
tasks: list[Task] = field(default_factory=list)
def add(self, title: str, priority: str, due_days: int) -> None:
new_id = max([t.id for t in self.tasks], default=0) + 1
self.tasks.append(Task(new_id, title, priority, due_days))
def complete(self, task_id: int) -> bool:
for task in self.tasks:
if task.id == task_id:
task.completed = True
return True
return False
def get_by_priority(self, priority: str) -> list[Task]:
return [t for t in self.tasks if t.priority == priority]
def get_overdue(self) -> list[Task]:
return [t for t in self.tasks if t.is_overdue]
def summary(self) -> dict[str, int]:
return {
"总任务": len(self.tasks),
"已完成": sum(1 for t in self.tasks if t.completed),
"高优先": len(self.get_by_priority("high")),
"逾期": len(self.get_overdue()),
}
# 模拟数据
manager = TaskManager()
# 添加一些任务(有些已逾期)
manager.add("修复登录 Bug", "high", due_days=-2) # 已逾期 2 天
manager.add("写周报", "medium", due_days=0) # 今天到期
manager.add("团队周会", "high", due_days=1) # 明天到期
manager.add("优化数据库", "low", due_days=7) # 7 天后
manager.add("Code Review", "medium", due_days=3)
# 标记完成
manager.complete(2)
# 输出统计
print("=== 任务概览 ===")
summary = manager.summary()
for key, value in summary.items():
print(f"{key}: {value}")
print("\n=== 高优先级任务 ===")
for task in manager.get_by_priority("high"):
print(f"[{task.urgency_label}] {task.title}")
print("\n=== 逾期任务 ===")
for task in manager.get_overdue():
print(f"⚠️ {task.title} - 已逾期 {-task.due_days} 天")
预期输出:
=== 任务概览 ===
总任务: 5
已完成: 1
高优先: 2
逾期: 1
=== 高优先级任务 ===
[已逾期 -2 天] 修复登录 Bug
[还剩 1 天] 团队周会
=== 逾期任务 ===
⚠️ 修复登录 Bug - 已逾期 2 天
解释:这个工具能帮你在任务多的时候快速分类、找出紧急事项——这就是类型化代码的好处:添加新功能时不容易引入 bug。
💪 进阶 20 分钟:常见坑 + 调试技巧
坑 1:Optional 值直接使用
# ❌ 错误:没检查 None 就用
user: Optional[dict] = None
print(user["name"]) # TypeError!
# ✅ 正确:先检查
if user is not None:
print(user["name"])
else:
print("用户不存在")
坑 2:类型不匹配
# ❌ 错误:类型不对
count: int = "123" # 字符串不能赋值给 int
# ✅ 正确:显式转换
count: int = int("123")
坑 3:可变默认参数
# ❌ 错误:默认参数是可变对象
def add_task(tasks: list = []) -> None: # 危险!
tasks.append("新任务")
# ✅ 正确:用 None + 初始化
def add_task(tasks: list = None) -> None:
if tasks is None:
tasks = []
tasks.append("新任务")
坑 4:忽视泛型约束
from typing import TypeVar
T = TypeVar('T', int, float) # 约束只能是数字类型
# ❌ 错误:传入不支持的类型
def double(value: T) -> T:
return value * 2 # 如果 T 是 str,这行会报错
# ✅ 正确:明确你的类型约束
def double_numeric(value: int | float) -> int | float:
return value * 2
坑 5:dataclass 字段顺序
from dataclasses import dataclass
# ❌ 错误:必填字段放在有默认值的后面
@dataclass
class User:
name: str = "" # 有默认值
age: int # 没有默认值!会报错
# ✅ 正确:必填字段在前
@dataclass
class User:
age: int
name: str = ""
调试技巧:pdb 断点调试
import pdb
def buggy_function(items: list) -> int:
result = 0
pdb.set_trace() # 程序会停在这里,进入交互式调试
for item in items:
result += item
return result
# 常用 pdb 命令:
# n (next) - 执行下一行
# p 变量名 - 打印变量值
# c (continue) - 继续执行
# l (list) - 查看当前代码上下文
或者更简单的 print 调试法:
def find_user(users: list[dict], user_id: int) -> dict | None:
for user in users:
print(f"检查用户: {user}") # 快速定位
if user["id"] == user_id:
return user
return None
✏️ 练习题 + 作业题
练习 1(2 分钟):添加一个新任务
- 输入:在项目 3 的基础上添加一个任务
("复习笔记", "low", 5) - 预期输出:summary 中的「总任务」变成 6
- 提示:直接调用
manager.add()即可
练习 2(2 分钟):过滤高优先级
- 输入:在项目 2 的用户列表中,找出
task_count > 2的用户 - 预期输出:小明和小刚
- 提示:用列表推导式
[u for u in users if u.task_count > 2]
练习 3(3 分钟):修复逾期统计
- 输入:项目 3 的代码,
is_overdue属性逻辑有误(没考虑 completed) - 预期输出:已完成的任务不算逾期
- 提示:检查
is_overdue的条件顺序
练习 4(5 分钟):组合两个项目
- 输入:用项目 2 的 JSON 数据,转成项目 3 的 Task 格式
- 预期输出:能按项目 3 的方式统计和筛选
- 提示:需要转换
tasks列表为due_days和priority
练习 5(5 分钟):分析报错
- 输入:以下代码运行会报什么错?
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int = 0
p1 = Point() # 哪里错了?
- 预期输出:
x是必填参数,必须传入 - 提示:dataclass 的必填字段必须有值
作业:做一个「个人时间管理小工具」
需求描述:
做一个命令行工具,管理你的每日计划。
功能点:
1. 添加任务(标题 + 优先级 + 预计耗时分钟数)
2. 列出所有任务(显示完成状态、耗时、优先级)
3. 完成一个任务(标记完成 + 计算实际耗时)
4. 统计今日完成情况(总任务数、已完成数、总耗时)
加分项:
1. 任务按优先级排序(high > medium > low)
2. 导出今日总结为字符串
验收标准:
- 能跑起来(python main.py)
- 添加和完成任务功能正常
- 统计结果准确
- 代码有类型注解
提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
本文学到的 3 个核心点:
1. 类型注解让代码自己会说话,IDE 也能帮你检查错误
2. dataclass 是 Python 里最接近 TypeScript interface 的写法
3. 泛型让你的函数更通用,同时保持类型安全
延伸学习资源:
- Python 官方 typing 文档 - 完整的类型系统参考
- Real Python: Type Hints - 进阶类型用法
- 《Python Crash Course》第九章 - 类与类型
互动钩子:
你在写代码时遇到过「类型不匹配」的报错吗?是哪个场景?评论区聊聊,老粉优先回复!
📌 下章剧透:写好了类型安全的代码,怎么知道它真的没问题?下一章我们学 Vitest + Vue Test Utils,用测试给代码上保险。你会发现——有了类型 + 测试,debug 变得如此简单。

评论(0)