第8章 8.2 泛型与高级类型
🎯 为什么要学泛型?
上一章我们学会了 TypeScript 入门,能给变量标类型了。但你有没有遇到过这种情况——
写了一个函数,输入是数字返回数字,输入是字符串返回字符串,输入是数组返回数组。类型该怎么标?总不能写
number | string | string[] | number[]吧?
这就是泛型要解决的问题。
学完这章你能:
- 写出「通用」函数,一种写法适配所有类型
- 理解 TypeScript 内置的高级类型(Partial、Pick、keyof)
- 搞清楚联合类型和交叉类型的区别
🧱 基础:泛型是个啥?
泛型函数:会变身的参数
生活类比:想象一下有个「万能塑料袋」,装苹果就是苹果袋,装水就是水袋。泛型就是 TypeScript 里的「万能塑料袋」——函数参数的类型,等你调用时才知道。
为什么要用:避免写重复的函数,比如 filterNumbers() 和 filterStrings() 可以合并成一个。
\n\n
\n\n
\n\n
# Python 3.11+ 泛型语法
from typing import TypeVar, Generic
# 定义一个「万能」的泛型变量 T(就像给塑料袋贴个标签)
T = TypeVar('T')
# 这是一个泛型函数,输入啥类型就返回啥类型
def first_element(items: list[T]) -> T | None:
"""返回列表的第一个元素,没有就返回 None"""
return items[0] if items else None
# 试试看
numbers = [1, 2, 3, 4, 5]
words = ["苹果", "香蕉", "橙子"]
print(first_element(numbers)) # 输出: 3
print(first_element(words)) # 输出: 橙子
这行 T = TypeVar('T') 就是在声明:「我要用 T 这个字母代表一种类型,具体啥类型等调用的时候再说」。
泛型约束:给塑料袋加个功能限制
生活类比:万能塑料袋虽然啥都能装,但如果你要装「能吃东西的」,那就得限制——只能是食物。泛型约束就是加条件。
为什么要用:有时候需要对泛型类型做操作(比如访问 .length),但 TypeScript 不知道这个类型有没有这个属性,就需要约束。
from typing import TypeVar
T = TypeVar('T')
# 约束 T 必须有 length 属性(必须是「有长度的」类型)
def get_length(item: T) -> int:
"""这行会报错!因为 T 不一定有 length"""
return len(item) # 报错:TypeError: object of type 'T' has no len()
上面这个例子会报错,因为 TypeScript 在编译时不知道 T 是什么类型,len() 能不能用。
from typing import TypeVar, Generic
T = TypeVar('T')
class HasLength:
"""有个长度的东西"""
def __init__(self, value: str | list):
self.value = value
def length(self) -> int:
return len(self.value)
# 用泛型约束限制 T 必须是 HasLength 的子类
def print_length(item: T) -> None:
print(f"长度是: {item.length()}")
# 测试一下
has_length_str = HasLength("hello")
has_length_list = HasLength([1, 2, 3])
print_length(has_length_str) # 输出: 长度是: 5
print_length(has_length_list) # 输出: 长度是: 3
多个泛型参数:不止一个变身
from typing import TypeVar
K = TypeVar('K')
V = TypeVar('V')
def make_pair(key: K, value: V) -> tuple[K, V]:
"""打包成元组返回"""
return (key, value)
result1 = make_pair("name", "小明")
result2 = make_pair(1, True)
print(result1) # 输出: ('name', '小明')
print(result2) # 输出: (1, True)
🧱 基础:高级类型(工具箱里的神器)
keyof:把所有键抽出来
生活类比:keyof 就像从一栋楼里抽出门牌号。「keyof Person」就是从 Person 类型里抽出所有属性名。
from typing import TypeVar, Literal
T = TypeVar('T')
K = Literal["name", "age", "city"]
# 模拟 keyof 的效果
def get_property(obj: dict, key: K) -> any:
"""根据 key 获取属性值"""
return obj.get(key)
person = {"name": "小明", "age": 25, "city": "北京"}
print(get_property(person, "name")) # 输出: 小明
print(get_property(person, "age")) # 输出: 25
Partial:所有属性变可选
生活类比:填表时有些字段可以不填。Partial 就是把一个类型的所有属性都变成可选的。
from typing import Optional
# 模拟 Partial 的效果
class User:
def __init__(self, name: str, age: int, email: str):
self.name = name
self.age = age
self.email = email
@classmethod
def partial(cls, data: dict) -> "User":
"""用字典部分字段创建 User(可选字段默认为 None)"""
return cls(
name=data.get("name", ""),
age=data.get("age", 0),
email=data.get("email", "")
)
# 全字段创建
full_user = User("小明", 25, "xiaoming@example.com")
print(f"完整用户: {full_user.name}, {full_user.age}")
# 部分字段创建
partial_user = User.partial({"name": "小红"})
print(f"部分用户: {partial_user.name}, {partial_user.age}") # age 默认为 0
Required:所有属性变必填
跟 Partial 相反,Required 把所有可选属性变成必填。
Pick:挑几个属性出来
from typing import TypeVar, Literal
# 模拟 Pick 的效果:只保留某些字段
def pick_fields(obj: dict, fields: list[str]) -> dict:
"""从对象里挑出指定字段"""
return {k: v for k, v in obj.items() if k in fields}
person = {"name": "小明", "age": 25, "email": "xiaoming@example.com", "city": "北京"}
# 只挑 name 和 email
short = pick_fields(person, ["name", "email"])
print(short) # 输出: {'name': '小明', 'email': 'xiaoming@example.com'}
联合类型:或的关系
生活类比:联合类型就像「或门」——可以是苹果或者香蕉,但不能是苹果和香蕉的混合物。
from typing import Union
# 联合类型:可能是字符串也可能是数字
def process(value: Union[str, int]) -> str:
if isinstance(value, str):
return f"字符串长度: {len(value)}"
else:
return f"数字翻倍: {value * 2}"
print(process("hello")) # 输出: 字符串长度: 5
print(process(42)) # 输出: 数字翻倍: 84
交叉类型:且的关系
生活类比:交叉类型就像「与门」——必须同时满足两个类型,就像你既是学生又是未成年。
from typing import Protocol
class HasName:
def __init__(self, name: str):
self.name = name
class HasAge:
def __init__(self, age: int):
self.age = age
# 交叉类型:同时有 name 和 age
class Student(HasName, HasAge):
def __init__(self, name: str, age: int, grade: str):
super().__init__(name)
HasAge.__init__(self, age)
self.grade = grade
student = Student("小明", 15, "初三")
print(f"{student.name} 今年 {student.age} 岁,在读 {student.grade}")
🔥 实战:3 个递进小项目
项目 1(5 分钟):泛型版数组工具箱
跟着抄就能跑,理解核心 API。
from typing import TypeVar, Generic
T = TypeVar('T')
class ArrayTool(Generic[T]):
"""泛型数组工具箱"""
def __init__(self, items: list[T]):
self.items = items
def first(self) -> T | None:
"""获取第一个元素"""
return self.items[0] if self.items else None
def last(self) -> T | None:
"""获取最后一个元素"""
return self.items[-1] if self.items else None
def filter(self, condition: callable) -> "ArrayTool[T]":
"""按条件过滤"""
return ArrayTool([item for item in self.items if condition(item)])
# 测试
numbers = ArrayTool([1, 2, 3, 4, 5])
words = ArrayTool(["苹果", "香蕉", "橙子"])
print(numbers.first()) # 输出: 1
print(words.last()) # 输出: 橙子
print(numbers.filter(lambda x: x > 2).items) # 输出: [3, 4, 5]
预期输出:
1
橙子
[3, 4, 5]
一句话解释:创建了一个「泛型工具箱」,处理数字列表和字符串列表用的是同一套代码。
项目 2(15 分钟):从 JSON 数据中提取信息
加入真实场景需求,读数据、处理数据。
from typing import TypeVar, Generic, Optional
import json
T = TypeVar('T')
class DataExtractor(Generic[T]):
"""从 JSON 数据中提取信息"""
def __init__(self, data: dict):
self.data = data
def get(self, key: str, default: Optional[T] = None) -> Optional[T]:
"""安全获取嵌套属性(支持点号语法)"""
keys = key.split('.')
current = self.data
for k in keys:
if isinstance(current, dict) and k in current:
current = current[k]
else:
return default
return current
def filter_by(self, key: str, value: any) -> list[dict]:
"""筛选出指定属性等于某值的记录"""
return [
item for item in self.data
if isinstance(item, dict) and item.get(key) == value
]
# 模拟从 API 拿到的数据
api_response = json.loads('''
[
{"id": 1, "name": "iPhone 15", "category": "手机", "price": 6999},
{"id": 2, "name": "MacBook Pro", "category": "电脑", "price": 12999},
{"id": 3, "name": "AirPods Pro", "category": "耳机", "price": 1899},
{"id": 4, "name": "小米 14", "category": "手机", "price": 3999}
]
''')
extractor = DataExtractor(api_response)
# 获取第一个商品的名称
first_name = extractor.get("0.name")
print(f"第一个商品: {first_name}")
# 筛选所有手机
phones = extractor.filter_by("category", "手机")
print(f"手机分类: {[p['name'] for p in phones]}")
# 获取不存在的属性,给默认值
nonexistent = extractor.get("0.non_existent", "默认值")
print(f"不存在的属性: {nonexistent}")
预期输出:
第一个商品: iPhone 15
手机分类: ['iPhone 15', '小米 14']
不存在的属性: 默认值
一句话解释:get() 方法用点号语法可以读取嵌套属性,再也不怕 KeyError 了。
项目 3(15 分钟):做一个配置合并工具
组合前两个项目的能力,做个有点真实用的小工具。
from typing import TypeVar, Generic, Optional, Union
import json
T = TypeVar('T')
class ConfigMerger:
"""配置合并工具 - 演示交叉类型"""
@staticmethod
def merge(base: dict, override: dict) -> dict:
"""合并两个配置,override 的值会覆盖 base 的值"""
result = base.copy()
for key, value in override.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
# 如果两者都是字典,递归合并
result[key] = ConfigMerger.merge(result[key], value)
else:
result[key] = value
return result
class TypedConfig(Generic[T]):
"""带类型检查的配置读取器"""
def __init__(self, config: dict, schema: dict):
self.config = config
self.schema = schema
def get(self, key: str, expected_type: type) -> Optional[T]:
"""按类型安全获取配置"""
value = self.config.get(key)
if value is None:
return None
if not isinstance(value, expected_type):
print(f"⚠️ 类型警告: {key} 期望 {expected_type.__name__},实际 {type(value).__name__}")
return None
return value
def validate(self) -> tuple[bool, list[str]]:
"""验证配置是否符合 schema 要求"""
errors = []
for key, expected_type in self.schema.items():
if key not in self.config:
errors.append(f"缺少必填配置: {key}")
elif not isinstance(self.config[key], expected_type):
errors.append(f"{key} 类型错误,期望 {expected_type.__name__}")
return len(errors) == 0, errors
# 模拟使用场景:游戏配置文件
default_config = {
"audio": {"music": 80, "sfx": 70},
"video": {"resolution": "1920x1080", "fps": 60},
"difficulty": "normal"
}
user_config = {
"audio": {"music": 50}, # 只覆盖 music,sfx 保留默认值
"difficulty": "hard"
}
# 合并配置
merged = ConfigMerger.merge(default_config, user_config)
print("合并后的配置:")
print(json.dumps(merged, indent=2))
# 类型安全读取
schema = {"audio": dict, "video": dict, "difficulty": str}
typed = TypedConfig(merged, schema)
music_volume = typed.get("audio.music", int) # 注意:这里简化了,实际需要更复杂的路径解析
print(f"\n音乐音量: {merged['audio']['music']}")
# 验证配置
is_valid, errors = typed.validate()
print(f"配置验证: {'通过' if is_valid else '失败'}")
if errors:
for error in errors:
print(f" - {error}")
预期输出:
合并后的配置:
{
"audio": {
"music": 50,
"sfx": 70
},
"video": {
"resolution": "1920x1080",
"fps": 60
},
"difficulty": "hard"
}
音乐音量: 50
配置验证: 通过
一句话解释:配置合并用递归处理嵌套字典,TypedConfig 帮你检查配置类型对不对。
💪 进阶:常见坑 + 小贴士
坑 1:泛型类型推断失败
# ❌ 错误示例:TypeScript 里这样写会报错
# def identity(x: T) { return x } // T 未定义
# ✅ 正确示例:必须先声明 TypeVar
from typing import TypeVar
T = TypeVar('T')
def identity(x: T) -> T:
return x
坑 2:Union 和 str | int 混用
from typing import Union
# ❌ 错误:在不支持 | 语法的旧版 typing
# def foo(x: str | int): pass
# ✅ 正确:用 Union
def foo(x: Union[str, int]) -> str:
return str(x)
坑 3:泛型约束忘记加边界检查
# ❌ 错误示例
T = TypeVar('T')
def first_or_default(items: list[T]) -> T:
return items[0] # 空列表会报错!
# ✅ 正确示例
def first_or_default(items: list[T], default: T) -> T:
return items[0] if items else default
坑 4:Partial 和必填属性混用
# ❌ 错误示例:有些字段必填,有些可选,容易混淆
class User:
def __init__(self, name: str, email: Optional[str] = None):
self.name = name
self.email = email
# ✅ 正确示例:用 @overload 定义多种调用方式
from typing import overload
class User2:
@overload
def __init__(self, name: str, email: str):
...
@overload
def __init__(self, name: str, email: None):
...
def __init__(self, name: str, email: str | None = None):
self.name = name
self.email = email
坑 5:交叉类型和接口继承分不清
# ❌ 错误示例:滥用交叉类型
# class A(B, C): pass # 继承两个类
# ✅ 正确示例:用接口继承
class Base:
pass
class Derived(Base):
pass
调试技巧:print 大法
# 泛型不工作?先 print 看看类型
T = TypeVar('T')
def debug_type(x: T) -> T:
print(f"传入类型: {type(x).__name__}") # 运行时打印类型
return x
debug_type("hello") # 输出: 传入类型: str
debug_type(123) # 输出: 传入类型: int
✏️ 练习题
练习 1(1 分钟):抄改泛型函数
- 输入:
[10, 20, 30]和["a", "b", "c"] - 预期输出:
30和c - 提示:把项目 1 的
last()方法拿来用,改个变量名就行
练习 2(2 分钟):加个条件判断
- 输入:
[1, 2, 3, 4, 5],条件是x > 3 - 预期输出:
[4, 5] - 提示:在项目 1 的
filter后面再加一行判断
练习 3(2 分钟):换个数据源
- 输入:
[{"name": "A", "score": 90}, {"name": "B", "score": 80}] - 预期输出:筛选出
score > 85的记录 - 提示:用项目 2 的
filter_by方法,改一下字段名
练习 4(3 分钟):串联两个项目
- 输入:产品列表,筛选出价格 > 5000 的,然后取最贵的那个
- 预期输出:价格最高的产品对象
- 提示:先 filter_by,再 first()
练习 5(2 分钟):分析报错
- 输入:下面这段代码为什么报错?
T = TypeVar('T')
def broken(items: list[T]) -> T:
return items[0]
- 预期输出:指出问题所在
- 提示:空列表的情况没考虑
📚 作业:做一个「第8章 8.2 泛型与高级类型实战工具」
需求描述:做一个「数据转换器」,能读取 JSON 配置文件,合并默认配置,按 schema 验证类型,并支持查询。
功能点:
1. 读取 JSON 配置文件
2. 合并默认配置(默认配置在代码里定义)
3. 按 schema 验证类型
4. 安全查询嵌套属性
加分项:
1. 支持从文件读取配置
2. 支持导出合并后的配置到文件
验收标准:
- 能跑起来
- 输出符合预期
- 代码有注释
提交方式:评论区贴代码或 GitHub 链接
📚 总结
这一章我们学了 3 个核心点:
1. 泛型函数:一种写法适配所有类型
2. 高级类型:Partial、Pick、keyof 让类型操作更灵活
3. 联合 vs 交叉:「或」和「且」的区别
推荐资源:
- TypeScript 官方文档 - 泛型
- 《TypeScript 入门与实战》- 相关章节
- B站视频:TypeScript 泛型详解
互动钩子:你在实际项目中用过泛型吗?有没有遇到过泛型类型推断翻车的情况?评论区聊聊,老粉优先回复!
下一章我们要学习 Node.js 基础,对比它和浏览器里 JavaScript 的区别。学完你就知道为什么
console在浏览器和 Node 里都能用,但document只有浏览器认识。

评论(0)