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 作为联合成员时会被分发跳过。

📖 小节

📝 作业

  1. 基础题(难度⭐):用条件类型实现 IsArray<T>——T 是数组类型返回 true,否则返回 false。测试 IsArray<string[]>IsArray<number>
  2. 进阶题(难度⭐⭐):用映射类型实现 Stringify<T>——把对象所有属性的类型变成 string。例如 { age: number }{ age: string }
  3. 挑战题(难度⭐⭐⭐):用 infer + 条件类型实现 DeepPromise<T>——递归解包嵌套 Promise,直到得到非 Promise 类型。测试 DeepPromise<Promise<Promise<Promise<number>>>> 应为 number。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏