0.8 类型系统

概述

你已经熟悉 TypeScript:类型注解、interface、泛型、Optionaltsc 编译期检查。Python 从 3.5+ 开始支持类型注解(type hints,PEP 484),但有一个关键差异:Python 类型注解默认不做任何运行时/编译期检查,需要用外部工具(mypy、pyright)来校验。这一章帮你把 TS 的类型思维映射到 Python。

JS/TS ↔ Python 对照表

概念

TypeScript

Python

说明

类型注解

let x: number

x: int

语法类似,冒号

返回值类型

function f(): string

def f() -> str:

-> 箭头

可选类型

string | undefined / Optional<string>

str | None / Optional[str]

| 或 Optional

联合类型

string | number

str | int

Python 3.10+ 支持 |

列表类型

number[] / Array<number>

list[int]

泛型方括号

字典类型

Record<string, number>

dict[str, int]

泛型方括号

元组类型

[string, number]

tuple[str, int]

定长元组

任意类型

any

Any

from typing import Any

不指定

unknown

无注解(隐式 Any)

不写就是动态

泛型

<T>(x: T) => T

TypeVar / Generic[T]

概念一致,语法不同

接口/形状

interface User { name: string }

TypedDict / Protocol

见下文

联合别名

type ID = string | number

type ID = str | int(3.12+)

type 语句

类型别名

type Point = { x: number }

Point = tuple[int, int]type

简单赋值

类型检查工具

tsc(编译时)

mypy / pyright(静态检查)

独立工具

运行时校验

无(需 zod)

无(需 pydantic)

都是第三方的活

强制类型转换

as Type

cast(Type, x)

仅给检查器看

空值安全

?. 可选链

无原生可选链(需手动判断)

Python 没有 ?.

核心概念

1. TypeScript vs Python 类型注解(PEP 484)

最大的认知差异:TS 类型是强制的、编译时检查;Python 类型注解是"注释",运行时不检查、不强制

// TS:类型错误会导致编译失败
let count: number = "hello";  // ❌ 编译错误
# Python:类型注解不阻止运行
count: int = "hello"   # ✅ 正常运行,不会报错
print(count)           # hello
  • Python 的类型注解只是元数据,解释器完全忽略它。

  • 要真正检查,需要外部工具:mypy(微软/社区主流)或 pyright(微软,VSCode 的 Pylance 背后)。

2. 基本类型注解

// TS
let name: string = "Alice";
let age: number = 30;
let active: boolean = true;
let items: number[] = [1, 2, 3];
# Python
name: str = "Alice"
age: int = 30
active: bool = True
items: list[int] = [1, 2, 3]

类型名对照:stringstrnumberint/floatbooleanbool

函数注解:

// TS
function add(a: number, b: number): number {
  return a + b;
}
# Python
def add(a: int, b: int) -> int:
    return a + b

3. Optional / Union 用 |

// TS
let id: string | null = null;
let value: string | number = 42;
# Python 3.10+
id_: str | None = None
value: str | int = 42
  • | 语法是 Python 3.10+ 推荐的现代写法。

  • 旧写法:Optional[str](= str | None)、Union[str, int],新代码直接用 |

4. 泛型 typing.Generic vs TS 泛型

// TS
function identity<T>(x: T): T {
  return x;
}
# Python
from typing import TypeVar

T = TypeVar("T")

def identity(x: T) -> T:
    return x

泛型类:

// TS
class Box<T> {
  constructor(public value: T) {}
}
# Python
from typing import Generic, TypeVar

T = TypeVar("T")

class Box(Generic[T]):
    def __init__(self, value: T):
        self.value = value

box: Box[int] = Box(42)
  • TypeVar 定义类型变量,Generic[T] 声明泛型类。

  • Python 3.12 有更简洁的 class Box[T]: 新语法。

5. TypedDict vs interface

描述"对象的形状"(键值对结构),用 TypedDict

// TS:interface
interface User {
  name: string;
  age: number;
}
const u: User = { name: "Alice", age: 30 };
# Python:TypedDict
from typing import TypedDict

class User(TypedDict):
    name: str
    age: int

u: User = {"name": "Alice", "age": 30}
  • TypedDict 只描述 dict 的形状,不能定义方法(对应 TS 的 interface 作为数据形状的用法)。

  • 可选键用 total=FalseNotRequired

6. Protocol vs interface

要描述"带方法的接口"(鸭子类型),用 Protocol

// TS:interface 描述行为
interface HasName {
  name: string;
  getName(): string;
}
# Python:Protocol 结构化类型
from typing import Protocol

class HasName(Protocol):
    name: str
    def get_name(self) -> str: ...

obj: HasName = SomeClass()   # 只要结构匹配即可
  • Protocol 实现结构化子类型(structural typing),类似 TS 的"结构兼容",不用显式 implements

  • 类比:TS 的 interface 是结构化类型,Python 的 Protocol 是它最接近的对应。

7. 运行时校验工具 mypy / pyright vs tsc

工具

作用

类比

tsc

TS 编译 + 类型检查

mypy / pyright(纯检查,不编译)

ESLint

风格 + 潜在 bug

ruff(兼 lint)

VSCode TS Server

编辑器实时提示

Pylance(基于 pyright)

# 安装与运行类型检查
uv add --dev mypy
uv run mypy src/          # 类似 npx tsc --noEmit
  • mypy 是最主流的类型检查器,pyright 更快(微软出品,Pylance 底层)。

  • 运行方式类似 tsc --noEmit:只检查,不改变运行时行为。

8. dataclass 配合类型

dataclass + 类型注解是 Python 里定义"结构化数据"的最佳组合(对应 TS 的 interface + 自动构造):

from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int
    email: str | None = None    # 可选字段带默认值

u = User("Alice", 30)
u2 = User("Bob", age=25, email="bob@example.com")

代码示例

从 TS 迁移一个用户模型到 Python

// TS
interface User {
  id: number;
  name: string;
  email?: string;
  roles: string[];
}
function getUserName(u: User): string {
  return u.name;
}
# Python
from dataclasses import dataclass

@dataclass
class User:
    id: int
    name: str
    email: str | None = None
    roles: list[str] = field(default_factory=list)

def get_user_name(u: User) -> str:
    return u.name

泛型函数对照

// TS
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}
# Python
from typing import TypeVar, Sequence

T = TypeVar("T")
def first(arr: Sequence[T]) -> T | None:
    return arr[0] if arr else None

最佳实践

  • 新代码尽量加类型注解,至少给公共函数签名数据类标注,配合 mypy/pyright 检查。

  • |list[int] 等现代语法(3.10+),别用老式 Optional/List

  • 数据容器用 @dataclass + 注解,字段默认值用 field(default_factory=...)

  • 描述纯数据形状用 TypedDict,描述行为接口用 Protocol,描述实体用 @dataclass

  • mypypyright 接入 CI(类似跑 tsc),从 strict 模式开始逐步收敛。

  • 运行时校验交给 pydantic(对应 zod),类型注解和运行时校验是两回事,别混淆。

前端开发者常见陷阱

  1. 以为类型注解会检查错误:Python 注解不检查、不强制,忘了这点会写出"看起来有类型其实没保障"的代码。

  2. listList 混淆:3.9+ 用内置 list[int],老代码用 typing.List,混用会困惑(新代码统一用内置)。

  3. None 可选没标str | None 漏写 | None,mypy 会报错(类似 TS 的 strict null check)。

  4. TypeVar 语法怪T = TypeVar("T") 字符串参数是为了反射,别疑惑为什么传字符串。

  5. TypedDict 不能有方法:它只是 dict 形状,要行为就用 Protocol 或普通 class。

  6. 没有可选链 ?.:Python 没有 ?.,要 obj and obj.attr 或显式 if obj is not None

  7. cast 不是真转换cast(int, x) 只骗过类型检查器,运行时不变换任何东西。

  8. Any 泛滥:图省事全标 Any,等于没类型,和 TS 的 any 一样是坏味道。

  9. dataclass 可变默认值roles: list[str] = [] 会报错/有陷阱,必须 field(default_factory=list)

  10. 类型注解与运行时值不一致:注解说 int,运行时传入 str 也不会报错,所以数据入口要用 pydantic 兜底。

总结

  • Python 类型注解(PEP 484)只是元数据,运行时不检查,检查靠 mypy/pyright(类比 tsc --noEmit)。

  • 语法对应:str/int/boollist[int]str | None-> int 与 TS 高度相似。

  • TypedDict ≈ 数据形状 interface,Protocol ≈ 行为 interface,@dataclass ≈ 实体定义。

  • 泛型用 TypeVar + Generic[T],3.12+ 有更简洁的 class Box[T] 语法。

  • 类型注解管静态检查,pydantic 管运行时校验,两者配合才完整。