第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\nSimple tech illustration expla\n\nAI comic creation scene, creat\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 里有 nullOptional[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_dayspriority

练习 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 变得如此简单。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。