CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
TypeScript 核心/课时/泛型
RawBlame
课时目录12 节

TypeScript 核心

  1. 01TypeScript 是什么
  2. 02基本类型
  3. 03函数类型
  4. 04接口与类型别名
  5. 05联合与交叉类型
  6. 06类型收窄
  7. 07泛型
  8. 08工具类型
  9. 09模块与 tsconfig
  10. 10React 组件 typing
  11. 11异步与 API 类型
  12. 12综合练习与 Next.js

泛型

泛型函数与接口、约束 extends。

35 分钟

泛型

这一课你将能做到

  1. 写出带类型参数的泛型函数 <T>,并理解类型参数如何被推断
  2. 定义泛型接口和泛型类,构建可复用的类型化容器
  3. 使用泛型约束 extends 和默认类型参数,处理「限制但不写死」的场景

为什么要学

前面学的类型都是「具体类型」:string、number、User。但真实世界里大量逻辑是通用的:一个 first() 函数该返回数组第一个元素——数组是 number[] 还是 User[] 无所谓,逻辑都一样。如果给每种数组都写一个版本,就是重复造轮子。

泛型(Generics)就是解决这个问题:把类型当成参数传进函数/接口/类,让同一段逻辑适配任意类型,同时保留类型信息。

没有泛型的 TS,很多代码你只能写成 any[]、Promise<any>、useState<any>,类型系统瞬间退化成摆设。学会泛型,你才真正能用 TS 写出既灵活又安全的代码。

先建立图像

泛型的直觉:函数接收「值参数」,泛型函数还额外接收「类型参数」。

普通函数:  function add(a, b) => 传入两个值,返回一个值

泛型函数:  function first<T>(arr: T[]) => 传入一个数组 + 一个类型 T
                                        T 由调用方决定
                                        返回类型是 T | undefined

            first<number>([1, 2, 3])    T = number,  返回 number | undefined
            first<string>(["a", "b"])   T = string,  返回 string | undefined
            first([true, false])        编译器自动推断 T = boolean

跟着做

新建 generics-demo.ts,逐步实操。

步骤 1:第一个泛型函数

// 没有泛型:只能处理 number 数组
function firstNumber(arr: number[]): number | undefined {
  return arr[0];
}

// 有泛型:什么数组都能处理
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const n = first<number>([1, 2, 3]);        // n: number | undefined
const s = first<string>(["a", "b"]);       // s: string | undefined
const b = first([true, false]);            // 自动推断 T = boolean

console.log(n, s, b); // 1 "a" true

<T> 就是类型参数(type parameter)。调用时可以不写,TS 会从实参推断。

步骤 2:多个类型参数

// 两个独立的类型参数
function pair<K, V>(key: K, value: V): [K, V] {
  return [key, value];
}

const p1 = pair("name", "小明");     // [string, string]
const p2 = pair("age", 25);          // [string, number]

console.log(p1, p2); // [ 'name', '小明' ] [ 'age', 25 ]

每个类型参数独立推断,K 和 V 可以相同也可以不同。

步骤 3:泛型约束 extends

类型参数不能乱传,要满足某种「能力」时用 extends 约束:

// 要求 T 必须有 length 属性
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(value: T): T {
  console.log(`长度:${value.length}`);
  return value;
}

logLength("hello");           // string 有 length,OK
logLength([1, 2, 3]);         // array 有 length,OK
// logLength(123);            // 报错:number 没有 length

步骤 4:泛型接口

// 通用 API 响应格式
interface ApiResponse<T> {
  code: number;
  message: string;
  data: T; // data 的类型由调用方决定
}

interface UserInfo {
  name: string;
  email: string;
}

const userResponse: ApiResponse<UserInfo> = {
  code: 0,
  message: "success",
  data: { name: "小明", email: "ming@example.com" },
};

const listResponse: ApiResponse<string[]> = {
  code: 0,
  message: "success",
  data: ["tag1", "tag2"],
};

console.log(userResponse.data.name); // 小明
console.log(listResponse.data);      // [ 'tag1', 'tag2' ]

步骤 5:泛型类

class Box<T> {
  private content: T;

  constructor(value: T) {
    this.content = value;
  }

  get(): T {
    return this.content;
  }

  set(value: T): void {
    this.content = value;
  }

  map<U>(fn: (value: T) => U): Box<U> {
    return new Box(fn(this.content));
  }
}

const numberBox = new Box<number>(42);
console.log(numberBox.get()); // 42

const stringBox = numberBox.map((n) => `数字是 ${n}`);
console.log(stringBox.get()); // 数字是 42

map<U> 是泛型方法:每次调用时可以有不同的 U,把 Box<number> 变成 Box<string>。

步骤 6:默认类型参数

// T 默认是 string,不传也能用
interface Paginated<T = string> {
  items: T[];
  total: number;
  page: number;
}

const page1: Paginated = { items: ["a", "b"], total: 2, page: 1 };
const page2: Paginated<number> = { items: [1, 2, 3], total: 3, page: 1 };

console.log(page1.items); // [ 'a', 'b' ]
console.log(page2.items); // [ 1, 2, 3 ]

步骤 7:泛型工具函数实战——类型安全的事件系统

type EventMap = {
  click: { x: number; y: number };
  scroll: { deltaY: number };
  keydown: { key: string };
};

class TypedEmitter<Events extends Record<string, unknown>> {
  private listeners = new Map<string, ((payload: never) => void)[]>();

  on<K extends keyof Events>(
    event: K,
    handler: (payload: Events[K]) => void
  ): void {
    const list = this.listeners.get(event as string) ?? [];
    list.push(handler as (payload: never) => void);
    this.listeners.set(event as string, list);
  }

  emit<K extends keyof Events>(event: K, payload: Events[K]): void {
    const list = this.listeners.get(event as string) ?? [];
    for (const handler of list) {
      handler(payload);
    }
  }
}

const emitter = new TypedEmitter<EventMap>();

emitter.on("click", (e) => {
  console.log(`点击坐标:${e.x}, ${e.y}`); // e 自动推断为 { x: number; y: number }
});

emitter.on("keydown", (e) => {
  console.log(`按下键:${e.key}`); // e 自动推断为 { key: string }
});

emitter.emit("click", { x: 100, y: 200 });
emitter.emit("keydown", { key: "Enter" });

这个例子覆盖了泛型的核心用法:约束 extends Record、keyof 索引、泛型事件名映射。理解了它,React/Vue 的类型推导对你就是透明的。

步骤 8:条件类型初探

// 条件类型:T extends U ? X : Y,根据类型关系选择结果
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false

// 实际用途:提取数组元素类型
type ElementType<T> = T extends (infer U)[] ? U : T;

type E1 = ElementType<string[]>; // string
type E2 = ElementType<number>;   // number

console.log("条件类型演示完成");

infer 是「类型推断」关键字:从已知类型中「提取」出未知部分。T extends (infer U)[] 意思是「如果 T 是某种数组,把数组元素类型提取到 U」。

讲清楚要点

  1. 泛型是类型层面的函数参数:值参数在运行时传入,类型参数在编译时传入。<T> 就是占位符,调用时确定具体类型。
  2. 能推断就不写:first([1, 2, 3]) 比 first<number>([1, 2, 3]) 更简洁。写死类型参数只在推断失败或需要显式收窄时。
  3. extends 是约束不是继承:T extends HasLength 读作「T 必须满足 HasLength 的形状」,跟 class 的 extends 语义不同。
  4. 泛型类和泛型接口的区别:泛型接口描述形状,编译后消失;泛型类有运行时实体,new Box<number>(42) 和 new Box<string>("a") 在运行时都是 Box 的实例。
  5. 类型参数名是约定:T = Type,K = Key,V = Value,E = Element,U = 另一个 Type。约定俗成,别瞎起。
  6. 泛型不承诺运行时类型检查:Box<string> 编译后就是 Box,你塞个数字进去,编译器拦得住,运行时(比如从 JSON 反序列化)拦不住。需要运行时校验请结合 zod 等库。

你可能会懵的地方

  • 泛型什么时候会「丢」类型? 类型断言 as any、类型断言到错误类型、或者从 any/unknown 直接赋值时,泛型的保护就失效了。编译器信你,但运行时不一定对。

  • <T,> 后面的逗号是什么? 在 .tsx 文件里,<T> 会被解析成 JSX 标签。加逗号 <T,> 告诉编译器这是泛型不是 JSX。

  • T extends object 和 T extends {} 有区别吗? object 排除原始类型;{} 在 TS 里几乎等于「非 null/undefined 的任何值」(包括 string、number)。{} 是历史遗留的宽松行为,尽量用 object 或更精确的约束。

  • 泛型函数能重载吗? 能。比如 function first<T>(arr: T[]): T | undefined 和 function first<T>(arr: T[], fallback: T): T 是两个重载签名。

  • 条件类型里 infer 能提取多个吗? 能:T extends [infer A, ...infer Rest] 可以提取元组第一个和剩余。这是高阶技巧,日常开发中用内置工具类型(Parameters、ReturnType)就够了。

错法对照

错法问题正解
到处用 any[] 代替泛型丢失元素类型,push 什么都行Array<T> 或 T[]
泛型参数不加约束直接调属性T 上不存在属性,编译报错T extends HasLength 或先用守卫收窄
在 .tsx 里写 <T> 泛型被解析成 JSX 标签报错加逗号 <T,> 或改用函数声明
把泛型类当类型用 Box 不带参数类型不完整Box<number> 或 Box<typeof value>
误以为泛型有运行时信息typeof box 拿不到 Box<number>,只有 Box运行时校验用 zod,别靠泛型

卡点急救

  1. 报错 Type 'T' does not satisfy the constraint:类型参数没满足约束。检查传入的类型是否缺少约束要求的属性,或把约束放宽。
  2. .tsx 文件里泛型箭头函数报错:<T> 被解析成 JSX。改成 <T,> 或 const fn = <T extends unknown>(...) => ...。
  3. 泛型类 new Box() 推断不出类型:构造函数参数没有用到 T。给构造函数加参数或显式标注 new Box<number>(42)。
  4. 条件类型推不出想要的类型:infer 的位置不对。检查模式匹配的结构是否和实际类型完全对应。

面试常问

  • 什么是泛型?解决什么问题? 泛型是类型参数化:把类型作为参数传给函数/接口/类,让同一段逻辑适配多种类型而不丢失类型信息。解决「通用逻辑 + 类型安全」的矛盾。

  • 泛型约束 extends 的作用? 限制类型参数必须满足某个形状(比如必须有 length 属性),让函数体内部能安全地访问该属性。没有约束时 T 只能当不透明类型用。

  • keyof 和泛型的关系? keyof 取对象类型的所有键组成联合类型,常和泛型配合:function get<T, K extends keyof T>(obj: T, key: K): T[K],实现类型安全的属性访问。

  • 泛型和 any 的区别? any 放弃检查,什么都能传、传完什么都能干;泛型保留类型,传入 number[] 返回就是 number | undefined,类型链路完整。泛型是类型安全的复用,any 是无类型的复用。

自测三问

  1. function first<T>(arr: T[]): T | undefined 中,<T>、T[]、T | undefined 各自代表什么?
  2. 为什么 T extends HasLength 之后函数体里就能访问 value.length?
  3. 泛型在运行时还存在吗?如果需要运行时类型校验,应该怎么办?

小结一句话

泛型 = 类型参数化:用 <T> 占位,调用时确定具体类型,让通用逻辑保留完整的类型链路——这是 TS 从「能写」到「写好」的分水岭。

延伸

  • Handbook: Generics
  • 下一课:工具类型——Partial/Pick/Omit/Record 等内置泛型工具,一行代码变换类型形状。
加载本节练习…
← 上一课类型收窄下一课 →工具类型