这篇主要对标 TypeScript:变量类型、函数类型、联合类型、字面量类型、interface、泛型、数据模型。
先记住一句话:TypeScript 是给 JavaScript 加静态类型;Python 类型注解是给 Python 代码加“类型说明”。它主要服务编辑器、类型检查工具和框架,不会默认在运行时帮你拦截错误。
一、先看一个完整例子
假设你在前端写一个订单摘要:
type UserRole = "admin" | "member" | "guest";
interface User {
id: number;
name: string;
role: UserRole;
}
interface CartItem {
productId: string;
title: string;
price: number;
count: number;
}
interface OrderSummary {
userId: number;
userName: string;
total: number;
itemTitles: string[];
}
function createOrderSummary(user: User, cartItems: CartItem[]): OrderSummary {
const total = cartItems.reduce((sum, item) => {
return sum + item.price * item.count;
}, 0);
return {
userId: user.id,
userName: user.name,
total,
itemTitles: cartItems.map((item) => item.title),
};
}
Python 里可以这样写:
from typing import Literal, TypedDict
UserRole = Literal["admin", "member", "guest"] # 类似 TS 的字面量联合类型
class User(TypedDict): # 类似 TS interface,用来描述 dict 的结构
id: int
name: str
role: UserRole
class CartItem(TypedDict):
product_id: str
title: str
price: float
count: int
class OrderSummary(TypedDict):
user_id: int
user_name: str
total: float
item_titles: list[str]
def create_order_summary(user: User, cart_items: list[CartItem]) -> OrderSummary:
total = 0.0
for item in cart_items:
total += item["price"] * item["count"]
return {
"user_id": user["id"],
"user_name": user["name"],
"total": total,
"item_titles": [item["title"] for item in cart_items],
}
从这个例子能先看到几个核心映射:
| TypeScript | Python |
|---|---|
number | int / float |
string | str |
boolean | bool |
null | None |
string[] | list[str] |
Record<string, number> | dict[str, int] |
"admin" | "user" | Literal["admin", "user"] |
interface User {} | class User(TypedDict): |
(value: number) => string | Callable[[int], str] |
二、Python 类型注解和 TS 最大的区别
TS 类型会参与编译检查
TypeScript 本身就是一个静态类型系统。你写错类型,tsc 会报错。
const age: number = "18"; // 报错,string 不能赋值给 number
Python 类型注解默认不拦截运行
Python 是动态语言,类型注解默认只是说明。
age: int = "18" # 代码能运行,但类型检查工具会提示不对
print(age) # 输出 "18"
也就是说,Python 类型注解主要有三个用途:
- 给自己和别人读代码时看清楚数据结构
- 给编辑器提供自动补全、跳转、错误提示
- 给
pyright/mypy这类工具做静态检查
如果你希望运行时也校验数据,通常要配合 Pydantic、dataclass、表单校验、接口参数校验等工具。
三、变量类型标注
Python 变量类型写在变量名后面。
name: str = "Alice"
age: int = 18
price: float = 99.8
is_active: bool = True
empty_value: None = None
前端类比:
const name: string = "Alice";
const age: number = 18;
const price: number = 99.8;
const isActive: boolean = true;
const emptyValue: null = null;
什么时候需要给变量写类型
简单变量可以不写,因为 Python 类型检查工具能推断:
name = "Alice" # 工具能推断 name 是 str
age = 18 # 工具能推断 age 是 int
但是这几种情况建议写:
- 空列表、空字典刚创建时
- 函数返回值接收后不容易看出类型时
- 业务模型字段比较复杂时
- 你希望编辑器后续自动补全时
user_ids: list[int] = []
user_score_map: dict[int, float] = {}
四、函数参数和返回值
Python 函数参数类型写在参数后面,返回值类型写在 -> 后面。
def add(a: int, b: int) -> int:
return a + b
result: int = add(1, 2)
前端类比:
function add(a: number, b: number): number {
return a + b;
}
没有返回值用 None
Python 函数没有写 return 时,真实返回值是 None。这点类似 JS 函数没有返回值时是 undefined。
def log_message(message: str) -> None:
print(message)
function logMessage(message: string): void {
console.log(message);
}
在 Python 里,-> None 更像 TS 的 void:告诉别人这个函数不是为了返回数据。
五、容器类型
Python 3.9+ 可以直接写 list[str]、dict[str, int] 这类类型。
names: list[str] = ["Alice", "Bob"]
scores: list[int] = [90, 95, 100]
user: dict[str, str | int] = {"id": 1, "name": "Alice"}
point: tuple[int, int] = (10, 20)
tags: set[str] = {"python", "backend"}
前端类比:
const names: string[] = ["Alice", "Bob"];
const scores: number[] = [90, 95, 100];
const user: Record<string, string | number> = { id: 1, name: "Alice" };
const point: readonly [number, number] = [10, 20];
const tags: Set<string> = new Set(["python", "backend"]);
常用对照
| 场景 | TypeScript | Python |
|---|---|---|
| 字符串数组 | string[] | list[str] |
| 数字数组 | number[] | list[int] / list[float] |
| 对象字典 | Record<string, string> | dict[str, str] |
| 固定长度元组 | [number, string] | tuple[int, str] |
| 集合 | Set<string> | set[str] |
六、联合类型和可选类型
Python 3.10+ 可以用 | 表示联合类型。
def format_user_id(user_id: int | str) -> str:
return str(user_id)
前端类比:
function formatUserId(userId: number | string): string {
return String(userId);
}
可选值就是和 None 联合
Python 没有 JS 里的 undefined。业务里表达“可能没有值”,最常见就是 None。
def find_user_name(user_id: int) -> str | None:
if user_id == 1:
return "Alice"
return None
前端类比:
function findUserName(userId: number): string | null {
if (userId === 1) {
return "Alice";
}
return null;
}
以前的写法也会看到:
from typing import Optional
name: Optional[str] = None # 等价于 str | None
现在更推荐直接写 str | None,更接近 TS 的联合类型写法。
七、Literal:字面量类型
TS 里经常用字符串字面量限制枚举值:
type VisitStatus = "pending" | "approved" | "rejected";
Python 里用 Literal:
from typing import Literal
VisitStatus = Literal["pending", "approved", "rejected"]
def get_status_text(status: VisitStatus) -> str:
if status == "pending":
return "待审核"
if status == "approved":
return "已通过"
return "已拒绝"
适合用 Literal 的场景:
- 状态值固定,比如
"pending"/"approved" - 类型值固定,比如
"person"/"car" - 请求参数只能传几个固定字符串
- 函数 mode 参数只能传几个固定选项
如果值很多,或者需要行为和属性,通常改用 Enum。
from enum import Enum
class VisitStatusEnum(str, Enum):
PENDING = "pending"
APPROVED = "approved"
REJECTED = "rejected"
八、TypedDict:对标 interface
如果你的数据本质是字典,比如接口返回、JSON 数据、配置对象,可以用 TypedDict 描述结构。
from typing import TypedDict
class UserPayload(TypedDict):
id: int
name: str
age: int
email: str | None
def normalize_user(user: UserPayload) -> str:
return f"{user['name']}({user['id']})"
前端类比:
interface UserPayload {
id: number;
name: string;
age: number;
email: string | null;
}
可选字段
TS 里写 email?: string,表示字段可以不存在。
interface UserPayload {
id: number;
name: string;
email?: string;
}
Python 3.11+ 可以用 NotRequired:
from typing import NotRequired, TypedDict
class UserPayload(TypedDict):
id: int
name: str
email: NotRequired[str]
这里要分清楚两个概念:
email: str | None # 字段必须存在,但值可以是 None
email: NotRequired[str] # 字段可以不存在;如果存在,值必须是 str
前端里也一样:
email: string | null; // 字段存在,值可能是 null
email?: string; // 字段可能不存在
九、dataclass:对标数据类,不等于 interface
如果你不想一直用字典访问,也可以定义数据对象。
from dataclasses import dataclass
@dataclass
class User:
id: int
name: str
age: int
user = User(id=1, name="Alice", age=18)
print(user.name)
它有点像 TS 里定义一个 class:
class User {
constructor(
public id: number,
public name: string,
public age: number,
) {}
}
dataclass 会自动生成构造函数、打印格式、比较方法等,适合纯数据对象。
但要注意:dataclass 默认也不会校验运行时类型。
user = User(id="1", name="Alice", age="18") # 仍然能创建
所以它更像“带类型说明的数据结构”,不是运行时表单校验器。
十、Pydantic:更接近运行时 schema
如果你写 FastAPI,或者要校验接口入参,Pydantic 很常见。
from pydantic import BaseModel
class CreateUserRequest(BaseModel):
name: str
age: int
email: str | None = None
payload = CreateUserRequest(name="Alice", age="18")
print(payload.age) # 18,Pydantic 会尝试转换类型
它和 TS 的区别要分清楚:
| 能力 | TypeScript interface | Python TypedDict | Python dataclass | Pydantic BaseModel |
|---|---|---|---|---|
| 描述字段结构 | 可以 | 可以 | 可以 | 可以 |
| 运行时校验 | 不可以 | 不可以 | 默认不可以 | 可以 |
| 自动类型转换 | 不可以 | 不可以 | 不可以 | 可以 |
| 适合接口入参 | 静态开发期 | 静态开发期 | 内部数据对象 | 很适合 |
如果按前端理解,Pydantic 更像:
TypeScript 类型 + zod/yup 表单校验 + DTO
所以 Python 后端项目里经常会看到:
类型注解
-> 描述代码里变量和函数的类型
Pydantic
-> 描述接口入参、出参,并做运行时校验
十一、Callable:函数类型
TS 里函数也可以作为参数:
function apply(value: number, handler: (value: number) => string): string {
return handler(value);
}
Python 里用 Callable:
from collections.abc import Callable
def apply(value: int, handler: Callable[[int], str]) -> str:
return handler(value)
def format_price(price: int) -> str:
return f"{price} 元"
text = apply(100, format_price)
Callable[[int], str] 的意思是:
接收一个 int 参数
返回 str
如果函数没有参数:
from collections.abc import Callable
def run_task(task: Callable[[], None]) -> None:
task()
如果参数不固定,临时可以写:
from collections.abc import Callable
from typing import Any
handler: Callable[..., Any]
但这个写法信息量很少,类似 TS 里写 (...args: any[]) => any,能不用就少用。
十二、泛型
泛型的作用是:把输入类型和输出类型关联起来。
TS 写法:
function first<T>(items: T[]): T {
return items[0];
}
const name = first(["Alice", "Bob"]); // string
const age = first([18, 20]); // number
Python 写法:
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
name = first(["Alice", "Bob"]) # 推断为 str
age = first([18, 20]) # 推断为 int
如果不用泛型,写成这样就丢失了关联:
from typing import Any
def first(items: list[Any]) -> Any:
return items[0]
这就类似 TS 里:
function first(items: any[]): any {
return items[0];
}
返回值变成 Any 后,编辑器就很难继续提示正确的方法。
十三、类型收窄
TS 会根据判断自动缩小类型范围:
function formatValue(value: string | number): string {
if (typeof value === "number") {
return value.toFixed(2);
}
return value.trim();
}
Python 里常用 isinstance 做类型收窄:
def format_value(value: str | int | float) -> str:
if isinstance(value, int | float):
return f"{value:.2f}"
return value.strip()
None 也经常需要先判断:
def get_name_length(name: str | None) -> int:
if name is None:
return 0
return len(name)
这个习惯很重要。你看到 str | None,就应该想到:使用前要处理 None 分支。
十四、Any 和 unknown 的区别
Python 的 Any 很像 TS 的 any。
from typing import Any
def print_value(value: Any) -> None:
print(value)
Any 的问题是:它会绕过类型检查。
from typing import Any
def get_user() -> Any:
return {"id": 1, "name": "Alice"}
user = get_user()
user.not_exists() # 类型检查工具可能不会拦你
在 TS 里,如果你想表达“不确定类型,但使用前必须检查”,会用 unknown。Python 标准类型里没有完全等价的 unknown,常见替代思路是:
- 能写清楚结构就别写
Any - 外部 JSON 先用
dict[str, Any]接住,再转换成明确模型 - 接口边界用 Pydantic 校验成明确类型
from typing import Any
raw_user: dict[str, Any] = {"id": 1, "name": "Alice"}
user: UserPayload = {
"id": int(raw_user["id"]),
"name": str(raw_user["name"]),
"age": 18,
"email": None,
}
十五、常见坑
坑一:类型注解不是运行时校验
def add(a: int, b: int) -> int:
return a + b
print(add("1", "2")) # 输出 "12",不会因为标注了 int 就自动报错
想发现这种问题,需要跑类型检查:
uv add --dev pyright
uv run pyright
或者:
uv add --dev mypy
uv run mypy .
坑二:空列表最好主动标注
user_ids = [] # 工具不好判断里面以后放什么
user_ids.append(1)
更清楚:
user_ids: list[int] = []
user_ids.append(1)
坑三:dict 的 key 要写清楚
user: dict[str, str] = {
"name": "Alice",
"city": "Shanghai",
}
如果 value 类型不统一,就用联合类型或 TypedDict:
user1: dict[str, str | int] = {
"id": 1,
"name": "Alice",
}
更推荐:
class UserInfo(TypedDict):
id: int
name: str
user2: UserInfo = {
"id": 1,
"name": "Alice",
}
坑四:不要过早把所有东西写成 Any
from typing import Any
def handle_user(user: dict[str, Any]) -> None:
print(user["name"])
这能跑,但类型信息很少。能建模型时,优先建模型:
class UserInfo(TypedDict):
id: int
name: str
def handle_user(user: UserInfo) -> None:
print(user["name"])
坑五:Python 字段命名通常用 snake_case
前端接口常见:
interface User {
userId: number;
userName: string;
}
Python 内部更常见:
class User(TypedDict):
user_id: int
user_name: str
如果接口字段是 camelCase,后端内部要不要转成 snake_case,看项目约定。FastAPI + Pydantic 里通常可以用 alias 处理。
十六、工程里怎么写更实用
普通业务函数
参数和返回值尽量写清楚。
def calculate_total(items: list[CartItem]) -> float:
total = 0.0
for item in items:
total += item["price"] * item["count"]
return total
接口入参
FastAPI 项目里更常用 Pydantic。
from pydantic import BaseModel
class CreateOrderItem(BaseModel):
product_id: str
count: int
class CreateOrderRequest(BaseModel):
user_id: int
items: list[CreateOrderItem]
coupon_code: str | None = None
内部数据结构
如果只是函数之间传递数据,可以用 TypedDict 或 dataclass。
from dataclasses import dataclass
@dataclass
class PriceResult:
total: float
discount: float
payable: float
选择建议:
| 场景 | 推荐 |
|---|---|
| JSON / dict 结构 | TypedDict |
| 内部纯数据对象 | dataclass |
| 接口入参和运行时校验 | Pydantic BaseModel |
| 固定状态字符串 | Literal 或 Enum |
| 回调函数 | Callable |
| 输入输出类型有关联 | 泛型 TypeVar |
十七、总结
Python 类型注解不要按“它会不会像 TS 一样严格”来理解,而要按工程用途理解:
- 它让函数签名更清楚
- 它让复杂 dict / list 结构更可读
- 它让编辑器能补全和提示
- 它让
pyright/mypy能提前发现问题 - 它和 Pydantic 搭配后,才更接近后端接口的运行时校验
前端迁移到 Python 时,可以先记这个顺序:
基础变量类型
-> 函数参数和返回值
-> list / dict / tuple / set
-> union 和 None
-> Literal 限制固定值
-> TypedDict 描述 JSON/dict
-> dataclass 描述内部数据对象
-> Pydantic 处理接口入参校验
-> Callable 和泛型处理更复杂的函数抽象