05-泛型编程
泛型编程
概述
Go 1.18 引入了泛型,但其表达能力仍然有限(不支持协变/逆变、条件类型等)。TypeScript 的泛型系统极其强大,支持泛型函数、泛型接口、泛型类、条件类型、映射类型、infer 关键字等高级特性。对于 Go 开发者,TS 泛型的灵活性可能令人惊叹,但也容易过度设计。
Go 开发者已知
// Go 1.18+ 泛型
func Map[T any, U any](s []T, f func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}
// 泛型约束
type Number interface {
~int | ~float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
// 泛型结构体
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}Go 泛型特点:
- 支持泛型函数和泛型类型
- 通过
interface定义类型约束(union) - 不支持协变/逆变
- 不支持条件类型
- 不支持映射类型
TypeScript 怎么做
泛型函数
// 基本泛型函数
function identity<T>(arg: T): T {
return arg
}
// 调用
const n = identity(42) // 推断为 number
const s = identity('hello') // 推断为 string
const x = identity<number>(42) // 显式指定
// 多个泛型参数
function map<T, U>(items: T[], fn: (item: T) => U): U[] {
return items.map(fn)
}泛型接口
interface Repository<T> {
getById(id: string): Promise<T | null>
getAll(): Promise<T[]>
create(item: T): Promise<T>
update(id: string, item: Partial<T>): Promise<T>
delete(id: string): Promise<void>
}
// 实现
class UserRepository implements Repository<User> {
async getById(id: string): Promise<User | null> {
return db.users.findById(id)
}
// ... 其他方法
}泛型类
class Stack<T> {
private items: T[] = []
push(item: T): void {
this.items.push(item)
}
pop(): T | undefined {
return this.items.pop()
}
peek(): T | undefined {
return this.items[this.items.length - 1]
}
get size(): number {
return this.items.length
}
}
const stack = new Stack<number>()
stack.push(1)
stack.push(2)
const top = stack.pop() // number | undefined泛型约束 extends
interface HasLength {
length: number
}
// 约束 T 必须具有 length 属性
function logLength<T extends HasLength>(arg: T): T {
console.log(arg.length)
return arg
}
logLength('hello') // 5
logLength([1, 2, 3]) // 3
// logLength(123) // Error: number 没有 length
// keyof 约束
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: 'Alice', age: 30 }
getProperty(user, 'name') // string
getProperty(user, 'age') // number
// getProperty(user, 'email') // Error条件类型 Conditional Types
// 条件类型
type IsString<T> = T extends string ? true : false
type A = IsString<'hello'> // true
type B = IsString<42> // false
// 嵌套条件类型
type TypeName<T> =
T extends string ? 'string' :
T extends number ? 'number' :
T extends boolean ? 'boolean' :
T extends undefined ? 'undefined' :
T extends Function ? 'function' :
'object'
type T1 = TypeName<string> // 'string'
type T2 = TypeName<42> // 'number'infer 关键字
// 提取函数返回类型
type ReturnType<T> = T extends (...args: unknown[]) => infer R ? R : never
type Fn = (x: number) => string
type R = ReturnType<Fn> // string
// 提取数组元素类型
type ElementType<T> = T extends (infer U)[] ? U : never
type E = ElementType<number[]> // number
// 提取 Promise 值类型
type Unwrap<T> = T extends Promise<infer U> ? U : T
type P = Unwrap<Promise<string>> // string
type N = Unwrap<number> // number映射类型 Mapped Types
// 将所有属性变为可选
type MyPartial<T> = {
[K in keyof T]?: T[K]
}
// 将所有属性变为只读
type MyReadonly<T> = {
readonly [K in keyof T]: T[K]
}
// 映射 + 条件类型
type Nullable<T> = {
[K in keyof T]: T[K] | null
}
// 应用
interface User {
name: string
age: number
}
type PartialUser = MyPartial<User>
// { name?: string; age?: number }差异分析
| 维度 | Go 1.18+ | TypeScript |
|---|---|---|
| 语法 | [T any] | <T> |
| 约束 | interface { ~int } | extends + 条件类型 |
| 条件类型 | 不支持 | T extends U ? X : Y |
| infer | 不支持 | 支持在条件类型中提取 |
| 映射类型 | 不支持 | [K in keyof T]: T[K] |
| 协变/逆变 | 不变 | 默认协变,可配置逆变 |
| 泛型约束符号 | ~ 近似 | extends 约束 |
| 内置工具类型 | 无 | Partial/Pick/Record 等 |
Go 的 ~ 近似约束 vs TS 的 extends
Go 的 ~int 表示"底层类型为 int 的所有类型"(包括自定义类型),而 TypeScript 的 extends 是结构化子类型检查。
// Go: 接受任何底层类型为 int 的类型
type MyInt int
func Add[T ~int](a, b T) T { return a + b }
// Add(MyInt(1), MyInt(2)) // OK// TS: 接受任何可赋值给 number 的类型
function add<T extends number>(a: T, b: T): T {
return (a + b) as T
}Bad Practice
// Bad: 泛型参数未使用
function process<T>(input: string): string {
return input.toUpperCase()
}
// Bad: 过度设计(过于泛化)
function deepClone<T, U, V>(
obj: T,
options?: U,
callback?: V
): T {
// 三个泛型参数只用了 T
return JSON.parse(JSON.stringify(obj))
}
// Bad: 泛型命名无意义
function f<A, B, C>(a: A, b: B): C {
// 难以理解
return a as unknown as C
}
// Bad: 泛型约束过于宽松
function getLength<T>(arg: T): number {
// return arg.length // Error: T 上不存在 length
return 0
}Best Practice
// Good: 有意义的泛型命名
function createPair<TKey, TValue>(key: TKey, value: TValue): [TKey, TValue] {
return [key, value]
}
// Good: 适当约束泛型参数
function getLength<T extends { length: number }>(arg: T): number {
return arg.length
}
// Good: 使用内置工具类型
interface User {
name: string
age: number
email: string
}
type PartialUser = Partial<User> // 所有属性可选
type UserName = Pick<User, 'name'> // 只取 name
type WithoutEmail = Omit<User, 'email'> // 排除 email
// Good: 条件类型做精确的类型转换
type Arrayify<T> = T extends unknown[] ? T : T[]
type A = Arrayify<string> // string[]
type B = Arrayify<number[]> // number[]
// Good: infer 提取复杂类型
type AsyncReturnType<T> = T extends (...args: unknown[]) => Promise<infer R> ? R : never
// Good: 映射类型保持修饰符
type Writable<T> = {
-readonly [K in keyof T]: T[K] // -readonly 去除只读
}内置工具类型速查
| 工具类型 | 作用 | 示例 |
|---|---|---|
Partial<T> | 所有属性可选 | Partial<User> |
Required<T> | 所有属性必填 | Required<PartialUser> |
Readonly<T> | 所有属性只读 | Readonly<User> |
Pick<T, K> | 选取部分属性 | Pick<User, 'name'> |
Omit<T, K> | 排除部分属性 | Omit<User, 'password'> |
Record<K, V> | 键值对对象 | Record<string, number> |
Exclude<T, U> | 从联合类型排除 | Exclude<'a'|'b', 'a'> |
Extract<T, U> | 从联合类型提取 | Extract<'a'|'b', 'a'> |
NonNullable<T> | 排除 null/undefined | NonNullable<string|null> |
ReturnType<T> | 函数返回值类型 | ReturnType<typeof fn> |
Parameters<T> | 函数参数类型元组 | Parameters<typeof fn> |
Awaited<T> | Promise 展开值类型 | Awaited<Promise<string>> |
code-tabs 对比
总结
| Go 泛型 | TypeScript 泛型 |
|---|---|
[T any] | <T> |
interface { ~int } 约束 | T extends SomeType |
| 仅支持基本泛型 | 支持条件/映射/infer 等高级特性 |
| 不变(invariant) | 协变/逆变可配置 |
| 没有内置工具类型 | 丰富的内置工具类型 |
TypeScript 的泛型系统比 Go 强大得多,但也需要克制。遵循 KISS 原则:能用具体类型解决问题就不要泛型化,能用一个泛型参数解决就不要用两个。
下一章将介绍 枚举与字面量类型。
附录 A:协变与逆变
定义
协变(Covariance) 和 逆变(Contravariance) 描述的是:当类型 A 是 B 的子类型时,Container<A> 与 Container<B> 之间是什么关系。
| 变体 | 含义 | 关系链 |
|---|---|---|
| 协变(Covariant) | A ≤ B ⇒ Container<A> ≤ Container<B> | 父子关系同向 |
| 逆变(Contravariant) | A ≤ B ⇒ Container<B> ≤ Container<A> | 父子关系反向 |
| 不变(Invariant) | Container<A> 与 Container<B> 无继承关系 | 固定不变 |
子类型(Subtype)的定义
我们说 A 是 B 的子类型(记作 A ≤ B),如果 A 类型的值可以安全地赋值给 B 类型的变量。在 TypeScript 中,这是通过结构化类型系统判定的:
// Cat ≤ Animal(Cat 是 Animal 的子类型)
interface Animal { eat(): void }
interface Cat extends Animal { meow(): void }
const cat: Cat = { eat() {}, meow() {} }
const animal: Animal = cat // OK:子类型可赋值给父类型TypeScript 的可变性规则
TypeScript 中不同位置的泛型参数有不同的默认可变性:
| 位置 | 默认可变性 | 说明 |
|---|---|---|
| 对象属性(读) | 协变 | readonly 属性安全 |
| 对象属性(写) | 协变 | 可变属性在结构类型下实际上也是协变的 |
| 函数返回值 | 协变 | 返回子类型总是安全的 |
| 函数参数 | 逆变(严格模式下) | 参数需要父类型约束才安全 |
| 数组 | 协变 | Cat[] 可赋值给 Animal[] |
函数参数的逆变
// 示例:函数参数是逆变的
type AnimalHandler = (a: Animal) => void
// Cat 是 Animal 的子类型
interface Animal { name: string }
interface Cat extends Animal { meow(): void }
// 函数类型的参数位置是逆变的
const handleCat: (c: Cat) => void = (c: Cat) => { c.meow() }
// 在 strictFunctionTypes 开启下,下面会报错
// const handleAnimal: AnimalHandler = handleCat
// Error: Cat ≤ Animal,但 (Cat) => void 不能赋值给 (Animal) => void
// 因为参数位置是逆变的,需要 (Animal) => void ≤ (Cat) => void 才安全为什么函数参数必须是逆变的?考虑以下场景:
// 假设没有逆变检查 —— 可能会有运行时错误
const animals: Animal[] = [new Cat(), new Cat()]
// 如果数组是协变的(TS 中就是),那么:
const catList: Cat[] = animals // TS 不允许!数组协变是不安全的
// 实际上 TS 允许数组协变,但标记为 unsound(已知的不安全行为)
// 但对于函数参数,TS 在 strictFunctionTypes 下会做正确检查:
type Logger = (msg: string) => void
// 协变(返回类型):安全 ✅
type StringLogger = () => string
type VoidLogger = () => void
const sl: StringLogger = () => 'hello'
const vl: VoidLogger = sl // OK:string 是 void 的子类型(协变)
// 逆变(参数类型):安全 ✅
type WideHandler = (input: string | number) => void
type NarrowHandler = (input: string) => void
const wh: WideHandler = (input) => { console.log(input) }
// const nh: NarrowHandler = wh
// Error: (string|number) => void 不能赋值给 (string) => void
// 因为参数需要的是逆变(WideHandler 的参数范围更大)对比 Go
Go 的泛型是**不变(Invariant)**的。这意味着 Stack[Cat] 和 Stack[Animal] 没有任何类型关系,即使 Cat 是 Animal 的子类型:
type Animal interface { Eat() }
type Cat struct{}
func (c Cat) Eat() {}
type Stack[T any] struct{ items []T }
var catStack Stack[Cat]
// var animalStack Stack[Animal] = catStack // 编译错误:类型不匹配记忆口诀
- 返回值类型:可以是子类型(更具体)→ 协变
- 参数类型:可以是父类型(更宽泛)→ 逆变
- 简记:"生产者协变,消费者逆变"(Producer Extends, Consumer Super — PECS 原则)
协变和逆变的实际用途
1. readonly 数组的协变安全性
// readonly 数组是类型安全的协变
interface Animal { name: string }
interface Cat extends Animal { meow(): void }
const cats: readonly Cat[] = [{ name: 'Kitty', meow() {} }]
const animals: readonly Animal[] = cats // OK:只读操作安全
// animals[0] = { name: 'Dog' } // Error:readonly 不可写
// 而可变数组协变虽然在 TS 中允许,但已知不安全
const mutableCats: Cat[] = [{ name: 'Kitty', meow() {} }]
const mutableAnimals: Animal[] = mutableCats // TS 允许(但可能不安全)
mutableAnimals.push({ name: 'Dog' }) // 运行时:数组里有 Dog 但类型标注为 Cat2. Vue 3 / React 中的协变模式
// Vue 3 的 defineComponent 利用协变
interface ComponentOptions<Props> {
props: Props
setup: (props: Props) => void
}
// 返回值协变 —— 安全
type RenderFunction = () => JSX.Element
type VueComponent = () => void
const render: RenderFunction = () => <div />
const comp: VueComponent = render // OK:返回值协变附录 B:泛型在函数式语言中的起源与全套特性
起源:从 System F 到 ML 家族
泛型的理论根基可以追溯到 System F(1972 年,Jean-Yves Girard),它引入了"参数多态"(Parametric Polymorphism)的概念——即一个函数可以对任意类型统一工作,而不是为每个类型单独实现。
| 时间 | 语言 / 系统 | 贡献 |
|---|---|---|
| 1972 | System F | 参数多态的形式化理论 |
| 1973 | ML | 首个引入参数多态的主流语言('a list) |
| 1990 | Haskell | 类型类(Typeclass)——带约束的多态 |
| 2004 | Scala | 协变/逆变注解(+T / -T) |
| 2012 | TypeScript | 结构化类型上的泛型 |
| 2015 | Rust | Trait 约束的泛型 |
| 2022 | Go 1.18 | 引入基本泛型 |
函数式泛型的完整特性
1. 参数多态(Parametric Polymorphism)
最基础的泛型形式:函数对所有类型统一工作,不依赖于具体类型的任何特性。
-- Haskell:参数多态
-- id 对任意类型 a 都有效
id :: a -> a
id x = x
-- 编译器无法知道 a 的任何信息
-- 因此 id 的实现被严格限制——只能返回输入// TypeScript 也支持参数多态
function identity<T>(arg: T): T {
return arg
// 在函数体内,T 是"完全未知"的
// 不能访问 .length、.toString() 等属性
}2. 带约束的多态(Constrained Polymorphism / Typeclass)
Haskell 的 Typeclass 将多态从"对任何类型工作"提升为"对满足某接口的任何类型工作":
-- Haskell Typeclass
class Show a where
show :: a -> String
-- 为自定义类型实现 Typeclass
data Person = Person { name :: String, age :: Int }
instance Show Person where
show (Person n a) = n ++ " (" ++ show a ++ ")"// TS 通过 extends 实现类似约束
interface Show {
toString(): string
}
function show<T extends Show>(value: T): string {
return value.toString()
}3. 高阶多态(Higher-Kinded Types / HKT)
HKT 是函数式语言最强大的泛型特性之一,允许对"类型构造器"(而非具体类型)进行抽象。这是 TypeScript 和 Go 都不支持的特性。
-- Haskell 中的 HKT
-- Functor 是一个类型类,它对"类型构造器 f"抽象
-- f 不是具体类型(如 Int、String),而是需要另一个类型参数的类型构造器(如 Maybe、List)
class Functor f where
fmap :: (a -> b) -> f a -> f b
-- Maybe 是 Functor
instance Functor Maybe where
fmap f (Just x) = Just (f x)
fmap f Nothing = Nothing
-- List 也是 Functor
instance Functor [] where
fmap = map
-- 使用:同一套接口操作不同的容器
addOne :: Functor f => f Int -> f Int
addOne = fmap (+1)
addOne (Just 5) -- Just 6
addOne [1, 2, 3] -- [2, 3, 4]
addOne (Right 5) -- Right 6 (Either 也是 Functor)// TypeScript 无法直接表达 HKT
// 以下代码无法编译:
// interface Functor<F> { // Error:F 需要是类型构造器
// fmap<A, B>(f: (a: A) => B, fa: F<A>): F<B>
// }
// 可以变通实现,但缺乏语言级支持
interface Functor<A> {
map<B>(f: (a: A) => B): Functor<B>
}
class Maybe<T> implements Functor<T> {
constructor(private value: T | null) {}
map<B>(f: (t: T) => B): Maybe<B> {
return this.value === null
? new Maybe<B>(null)
: new Maybe<B>(f(this.value))
}
}4. 类型级编程(Type-Level Programming)
函数式语言的泛型系统可以强大到在类型层面进行"计算"——TS 的条件类型、映射类型和 infer 正是受此启发:
-- Haskell 的类型族(Type Families)
-- 在类型层面做"计算"
type family Add (a :: Nat) (b :: Nat) :: Nat where
Add 'Zero b = b
Add ('Succ a) b = 'Succ (Add a b)
-- Add (Succ Zero) (Succ Zero) 计算结果为 Succ (Succ Zero)
-- 即:1 + 1 = 2(在类型层面)// TypeScript 的"类型级编程"等价物
// 同样是编译器在类型层面做计算
// 条件类型 —— 类型层面的 if/else
type IsString<T> = T extends string ? 'yes' : 'no'
// 递归条件类型 —— 类型层面的循环
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T
type Result = Flatten<number[][][]> // number
// infer —— 类型层面的模式匹配
type InferArrayItem<T> = T extends (infer U)[] ? U : never5. 存在类型(Existential Types)
某些函数式语言(如 Haskell 通过 ExistentialQuantification)支持"存在类型"——"存在某个类型 T,使得 ...",这在 TS 中可通过某些模式模拟:
-- Haskell 存在类型
data AnyShow = forall a. Show a => AnyShow a
-- 可以存放任何实现了 Show 的类型
items :: [AnyShow]
items = [AnyShow 42, AnyShow "hello", AnyShow 3.14]
showAll :: [AnyShow] -> String
showAll = concatMap (\(AnyShow a) -> show a)// TypeScript 没有直接的存在类型语法
// 但可以通过接口抽象实现类似效果
interface AnyShow {
show: () => string
}
const items: AnyShow[] = [
{ show: () => 42..toString() },
{ show: () => 'hello' },
{ show: () => 3.14.toString() },
]6. Ad-hoc 多态
与参数多态相对,Ad-hoc 多态允许同一函数名对不同类型有不同实现(即函数重载):
-- Haskell 的 Typeclass 实现 ad-hoc 多态
class Display a where
display :: a -> String
instance Display Int where
display n = "Int: " ++ show n
instance Display String where
display s = "String: " ++ s
-- 同一 display 函数对不同类型表现不同
display (42 :: Int) -- "Int: 42"
display ("hello") -- "String: hello"// TypeScript 通过函数重载实现 ad-hoc 多态
function display(value: number): string
function display(value: string): string
function display(value: number | string): string {
if (typeof value === 'number') return `Int: ${value}`
return `String: ${value}`
}泛型特性的跨语言对比
| 特性 | Haskell | TypeScript | Go | 说明 |
|---|---|---|---|---|
| 参数多态 | ✅ | ✅ | ✅ | 对所有类型统一工作 |
| 约束多态 | ✅ Typeclass | ✅ extends | ✅ interface | 对满足约束的类型工作 |
| 协变/逆变 | ✅ 声明侧(+T/-T) | ✅ 默认协变,strict 下参数逆变 | ❌ 不变 | 子类型关系的方向性 |
| 高阶多态(HKT) | ✅ | ❌(可模拟) | ❌ | 对类型构造器抽象 |
| 类型级编程 | ✅ 类型族 | ✅ 条件/映射/infer | ❌ | 在类型层面做计算 |
| 存在类型 | ✅ | ❌(可模拟) | ❌ | "存在某个类型 T" |
| Ad-hoc 多态 | ✅ Typeclass | ✅ 函数重载 | ❌ | 同函数名不同实现 |
| 类型推断 | ✅ 全局 Hindley-Milner | ✅ 局部推断 | ✅ 局部推断 | 自动推导类型 |
从函数式视角理解 TS 泛型
如果你熟悉 Haskell 或 ML,可以用以下映射来理解 TS 泛型:
Haskell TypeScript
─────────────────────────────────
'a. a -> a <T>(x: T) => T 参数多态
Show a => ... T extends Show ... 约束多态
data Maybe a class Maybe<T> 泛型类型
fmap :: ... map<B>(f: ...): ... 泛型方法
type family type X<T> = T extends ... 类型级计算为什么 HKT 在 TS 中很重要(但缺失)?
HKT 允许你抽象"容器类型"本身。例如在函数式编程中,你可以写一个泛型函数,它同时对 Array、Promise、Observable 等容器工作:
-- 如果 TS 支持 HKT,你可以写:
// function map<F, A, B>(f: (a: A) => B, fa: F<A>): F<B>
// 然后 map(x => x + 1, [1,2,3]) // [2,3,4]
// map(x => x + 1, Promise.resolve(1)) // Promise<2>当前 TS 不支持 HKT,但社区有 fp-ts 等库通过巧妙编码模拟了部分能力。
通过理解泛型在函数式语言中的起源,你可以更深刻地理解 TypeScript 泛型的设计哲学——它本质上是在一个面向对象的命令式语言中,引入了函数式语言的类型级抽象能力。这是 TypeScript 区别于 Java/C#/Go 等语言泛型系统的根本原因。