TypeScript: TypeScript 泛型进阶
最后更新:2026-08-26
掌握了泛型基础后,本课进入泛型的高级领域——条件类型、infer 推断、映射类型。这些是 TypeScript 类型编程的核心工具,也是理解内置工具类型源码的基础。
1. 条件类型(Conditional Types)
条件类型根据类型条件选择不同的结果——类似类型层面的三元运算符:
(1) 基本语法
TYPESCRIPT
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
type C = IsString<"hello">; // "yes"("hello" 是 string 的子类型)
(2) 条件类型与联合类型(分布式)
当 T 是联合类型时,条件类型会"分布式"地对每个成员分别判断:
TYPESCRIPT
type ToString<T> = T extends string ? "string" : "other";
// T = string | number → 分别判断 → "string" | "other"
type Result = ToString<string | number>; // "string" | "other"
(3) 禁用分布式行为
用 [T] 包裹禁用分布式判断:
TYPESCRIPT
type IsNever<T> = [T] extends [never] ? "yes" : "no";
type A = IsNever<never>; // "yes"
type B = IsNever<string | never>; // "no"(不分发了)
▶ 示例:类型安全的数组扁平化
TYPESCRIPT
type Flatten<T> = T extends Array<infer U> ? U : T;
type A = Flatten<string[]>; // string
type B = Flatten<number>; // number
type C = Flatten<boolean[]>; // boolean
2. infer 关键字
infer 在条件类型中"推断"一个类型变量——它是 TypeScript 类型编程最强大的工具之一:
(1) 推断数组元素类型
TYPESCRIPT
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type A = ArrayElement<string[]>; // string
type B = ArrayElement<number[]>; // number
type C = ArrayElement<string>; // never(string 不是数组)
(2) 推断函数返回值类型
TYPESCRIPT
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type A = GetReturnType<() => string>; // string
type B = GetReturnType<(x: number) => boolean>; // boolean
type C = GetReturnType<(x: string) => void>; // void
type D = GetReturnType<string>; // never(string 不是函数)
(3) 推断函数参数类型
TYPESCRIPT
type GetParameters<T> = T extends (...args: infer P) => any ? P : never;
type A = GetParameters<(a: string, b: number) => void>; // [string, number]
type B = GetParameters<() => void>; // []
type C = GetParameters<(x: boolean) => string>; // [boolean]
(4) 推断 Promise 解析值类型
TYPESCRIPT
type Awaited<T> = T extends Promise<infer U> ? U : T;
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<number[]>>; // number[]
type C = Awaited<string>; // string(不是 Promise,直接返回)
// 嵌套 Promise——递归解包
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type D = DeepAwaited<Promise<Promise<string>>>; // string
3. 映射类型(Mapped Types)
映射类型基于旧类型创建新类型——对每个属性做变换:
(1) 基本语法
TYPESCRIPT
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Optional<T> = {
[K in keyof T]?: T[K];
};
interface User {
name: string;
age: number;
email: string;
}
type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; readonly email: string }
type OptionalUser = Optional<User>;
// { name?: string; age?: number; email?: string }
(2) 映射修饰符
用 +/- 添加或移除修饰符:
TYPESCRIPT
// 移除 readonly
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
// 移除可选(?)
type Required2<T> = {
[K in keyof T]-?: T[K];
};
interface Config {
readonly host: string;
readonly port: number;
debug?: boolean;
}
type MutableConfig = Mutable<Config>;
// { host: string; port: number; debug?: boolean } —— readonly 被移除
type RequiredConfig = Required2<Config>;
// { readonly host: string; readonly port: number; debug: boolean } —— ? 被移除
(3) 键的重映射(Key Remapping,TypeScript 4.1+)
TYPESCRIPT
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }
(4) 过滤属性
TYPESCRIPT
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Mixed {
name: string;
age: number;
email: string;
active: boolean;
}
type StringProps = OnlyStrings<Mixed>;
// { name: string; email: string } —— 只保留 string 类型的属性
▶ 示例:构建类型安全的 API 客户端
TYPESCRIPT
// 定义 API 路由类型
type ApiRoutes = {
"/users": { response: { id: number; name: string }[] };
"/users/:id": { response: { id: number; name: string }; params: { id: number } };
"/posts": { response: { id: number; title: string }[] };
};
// 根据路由提取响应类型
type ApiResponse<R extends keyof ApiRoutes> = ApiRoutes[R]["response"];
type UsersResponse = ApiResponse<"/users">;
// { id: number; name: string }[]
type UserResponse = ApiResponse<"/users/:id">;
// { id: number; name: string }
// 类型安全的 fetch 函数
function fetchApi<R extends keyof ApiRoutes>(
route: R,
...args: "params" extends keyof ApiRoutes[R]
? [params: ApiRoutes[R]["params"]]
: []
): Promise<ApiResponse<R>> {
// 实现省略
return {} as any;
}
// 使用时自动推断参数和返回类型
// let users = fetchApi("/users"); // 不需要参数
// let user = fetchApi("/users/:id", { id: 1 }); // 必须提供 params
4. 泛型默认值
泛型类型参数可以有默认值——当推断不出或不需要指定时使用默认类型:
TYPESCRIPT
interface PaginatedResult<T, PageSize = 10> {
data: T[];
total: number;
pageSize: PageSize;
}
// 使用默认值
type UserResult = PaginatedResult<User>;
// data: User[]; total: number; pageSize: 10
// 覆盖默认值
type CustomResult = PaginatedResult<User, 20>;
// data: User[]; total: number; pageSize: 20
(1) 默认值的约束
有默认值的类型参数必须在没有默认值的参数后面:
TYPESCRIPT
// ✅ 正确
type A<T, U = string> = { first: T; second: U };
// ❌ 错误——有默认值的 U 在没有默认值的 T 前面
// type B<U = string, T> = { first: T; second: U };
5. 协变与逆变
这是 TypeScript 类型系统中最容易混淆的高级概念——理解它,才能理解为什么某些赋值合法而另一些不合法。
(1) 协变(Covariant)
"如果 A 是 B 的子类型,那么 Container<A> 也是 Container<B> 的子类型"——同方向变化:
TYPESCRIPT
// string 是 string | number 的子类型
// string[] 也是 (string | number)[] 的子类型 → 协变
let strings: string[] = ["a", "b"];
let mixed: (string | number)[] = strings; // ✅ 协变安全
(2) 逆变(Contravariant)
函数参数类型是逆变的——"如果 A 是 B 的子类型,那么 (B => void) 是 (A => void) 的子类型"——反方向变化:
TYPESCRIPT
// string 是 string | number 的子类型
// (string | number => void) 是 (string => void) 的子类型 → 逆变
type StringHandler = (arg: string) => void;
type MixedHandler = (arg: string | number) => void;
let mixedHandler: MixedHandler = (arg) => console.log(arg);
let stringHandler: StringHandler = mixedHandler; // ✅ 逆变——处理更宽类型的函数可以赋给处理更窄类型的变量
(3) 为什么函数参数是逆变
TYPESCRIPT
// 如果参数是协变的——不安全
let dogHandler: (dog: Dog) => void = (dog) => dog.bark();
let animalHandler: (animal: Animal) => void = dogHandler; // ❌ 危险!
// 调用 animalHandler(cat) → dog.bark() 在 cat 上调用 → 运行时错误
// 逆变才是安全的——处理更宽类型的函数可以安全地处理更窄类型
💡 简要理解: 函数返回值是协变的(更具体的返回类型安全),函数参数是逆变的(更宽泛的参数类型安全)。TypeScript 在
strictFunctionTypes 模式下严格执行逆变检查。
❓ 常见问题
Q infer 和泛型 T 有什么区别?
A 泛型 T 是调用者提供的类型参数,infer 是条件类型内部自动推断的类型变量。T 是"你告诉我",infer 是"我自己推断"。infer 只能在条件类型的 extends 子句中使用。
Q 映射类型和工具类型(Partial、Required 等)有什么关系?
A 工具类型就是用映射类型实现的。Partial、Required、Readonly、Pick、Omit 等都是 TypeScript 内置的映射类型。理解映射类型的原理,就能理解这些工具类型的工作方式,也能自己创建自定义工具类型。
Q 协变和逆变在日常开发中重要吗?
A 大多数时候不需要刻意考虑——TypeScript 的类型检查会自动处理。只有在写泛型函数、高阶函数或类型工具时才需要理解。初学者了解概念即可,遇到函数赋值类型不匹配时再深入分析。
Q 条件类型什么时候会"分发"?
A 当条件类型的 T 是裸类型参数(没有被包裹在元组、对象等中)且是联合类型时,会分布式判断。用
[T] extends [U] 包裹可以禁用分发。IsNever 的例子就是最常见的需要禁用分发的场景——never 作为联合成员时会被分发跳过。📖 小节
- 条件类型
T extends U ? X : Y是类型层面的三元运算符 - infer 在条件类型中推断类型变量——推断数组元素、函数返回值、Promise 解析值
- 映射类型基于旧类型创建新类型——
{ [K in keyof T]: T[K] },支持 +/- 修饰符和键重映射 - 泛型默认值
<T = DefaultValue>让泛型参数有后备类型 - 协变(同方向)和逆变(反方向)描述泛型类型的子类型关系——函数返回值协变,参数逆变
📝 作业
- 基础题(难度⭐):用条件类型实现
IsArray<T>——T 是数组类型返回 true,否则返回 false。测试IsArray<string[]>和IsArray<number>。 - 进阶题(难度⭐⭐):用映射类型实现
Stringify<T>——把对象所有属性的类型变成 string。例如{ age: number }→{ age: string }。 - 挑战题(难度⭐⭐⭐):用 infer + 条件类型实现
DeepPromise<T>——递归解包嵌套 Promise,直到得到非 Promise 类型。测试DeepPromise<Promise<Promise<Promise<number>>>>应为 number。