泛型与条件类型:TypeScript 类型体操的核心

1. 技术难点:类型系统为什么要"编程"

TypeScript 的类型系统并不是"给变量标个类型"这么简单------它本身是一门图灵完备的、可编程的类型语言。当你需要处理:

  1. 从接口/联合类型派生新类型:比如"排除某个字段"、"取出某属性类型"、"把数组元素类型拆出来"。
  2. 让类型随输入变化:传一个字符串返回字符串、传数组返回元素类型------这种"输入不同、输出不同"的类型逻辑。
  3. 条件分支:根据类型是否满足某个约束,决定最终类型。

新手只会用 string/number/interface 是够的,但到大型组件库、框架类型定义、工具类型(Partial、Pick、Omit)时,就必须懂类型编程 (俗称"类型体操")。核心三件套:泛型 + 条件类型 + 映射类型。

2. 完整解法:三个核心武器

2.1 泛型(Generic):让类型"可变"

泛型 = 类型的"参数"。定义时用 <T> 占位,使用时指明具体类型:

ts 复制代码
// 泛型函数:输入什么类型,返回什么类型
function identity<T>(value: T): T {
  return value;
}
const a = identity('hi');   // a: string(自动推断)
const b = identity(123);    // b: number

// 泛型接口 / 泛型类
interface ApiResponse<T> {
  code: number;
  data: T;
}
const resp: ApiResponse<{ id: number }> = {
  code: 0,
  data: { id: 1 },
};

泛型的价值:一次定义、多种类型复用,且类型安全和返回值关联(不再是 any)。

2.2 条件类型:类型世界的 if/else

用 T extends X ? A : B 表达"如果 T 满足 X 约束,类型是 A,否则是 B":

ts 复制代码
// 基本条件类型
type IsString<T> = T extends string ? true : false;
type A = IsString<'hi'>;    // type A = true
type B = IsString<number>;  // type B = false

// 结合 never:过滤类型
type Exclude<T, U> = T extends U ? never : T;
type C = Exclude<'a' | 'b' | 'c', 'a'>;  // type C = 'b' | 'c'

never 的妙用 :条件类型中返回 never 相当于"把这个类型从联合里过滤掉"。所以 Exclude、Extract、NonNullable 都是基于条件类型+never 实现的。

2.3 分布式条件类型(Distributive Conditional Types)

当 T 是联合类型 时,条件类型会自动对联合的每一项分别求值------这是类型体操的隐藏杀手:

ts 复制代码
// 如果直接 T extends ...,联合类型会逐项分发
type ToArray<T> = T extends unknown ? T[] : never;
type R = ToArray<'a' | 'b'>;   // type R = 'a'[] | 'b'[](分发,不是 ('a'|'b')[])

// 想整体处理不分发?用 [] 包裹:
type ToArrayNoDistribute<T> = [T] extends [unknown] ? T[] : never;
type S = ToArrayNoDistribute<'a' | 'b'>;  // type S = ('a'|'b')[]

踩坑点 :写条件类型时,如果不想要分发行为,一定要用 [T] 包裹,否则联合多元素时行为和你预期的不一样。

2.4 映射类型(Mapped Types):批量改造

用 keyof + 遍历键生成新类型:

ts 复制代码
// 遍历对象的所有键
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Partial<T> = { [K in keyof T]?: T[K] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };

// keyof + 条件类型:从一个类型提取符合某条件的键
type KeysOfType<T, U> = {
  [K in keyof T]: T[K] extends U ? K : never
}[keyof T];

interface User {
  name: string;
  age: number;
  isAdmin: boolean;
}
type StringKeys = KeysOfType<User, string>;  // type StringKeys = 'name'

[keyof T] 的用法:把"值"(联合)取出来,结合条件类型过滤键。

3. 实战:手写常用的工具类型(理解 TS 内置类型原理)

TS 内置的 Partial/Pick/Omit 等,都可以自己实现,从而理解它们的原理:

ts 复制代码
// Exclude:排除联合中的某些类型
type MyExclude<T, U> = T extends U ? never : T;

// Omit:从对象剔除某些键
type MyOmit<T, K extends keyof T> = {
  [P in MyExclude<keyof T, K>]: T[P];
};

// 条件 + 泛型:确保参数是数组并取元素类型
type Flatten<T> = T extends Array<infer E> ? E : T;
type El = Flatten<number[]>;  // type El = number(infer 提取数组元素)

// infer:在条件类型中"提取"子类型(高级但极有用)
type ReturnType<T> = T extends (...args: any) => infer R ? R : any;
type Fn = (x: number) => string;
type RT = ReturnType<Fn>;  // type RT = string

infer :允许你在条件类型里声明一个"待推断的类型变量",用于从结构里提取(数组元素、函数返回值、Promise 结果等)------这是 ReturnType、PromiseType 等高级工具的核心。

4. 要点总结

  • 泛型 :类型的参数化,<T> 占位复用,让返回值和输入关联。
  • 条件类型 :T extends X ? A : B 是类型的 if/else;返回 never 即过滤。
  • 分布式条件类型 :联合类型会自动逐项分发,不想分发用 [T] 包裹(隐藏坑)。
  • 映射类型 :keyof + 遍历键批量改造对象类型(Readonly/Partial/Pick 的原理)。
  • infer:条件类型里提取子类型,是 ReturnType/PromiseType 等高级工具的基石。
  • 实战价值:理解后能自己写工具类型、看懂框架(Vue/React/zustand)的类型定义、用强类型约束业务。

一句话:类型体操 = 泛型(参数)+ 条件类型(分支)+ 映射类型(批改)+ infer(提取)。这四个工具组合起来,TypeScript 的类型系统就成了你程序里的"第二套可验证的逻辑"。

相关推荐
丨白色风车丨4 分钟前
LangChain Agent 从入门到实战:创建与调用参数全解析
前端·python·langchain
Jmyd01238 小时前
Web端3D营销展厅技术实现路径:从素材上云到线索回流的数据链路
前端·3d·性能优化·三维数字化
砚凝霜8 小时前
【软考信息安全】第二十二章 Web服务器安全分析与增强——Apache与IIS
服务器·前端·安全
u01110267510 小时前
JPG、PNG、WebP 怎么选?按透明度、体积和使用场景做判断
前端·javascript·vue.js·图像处理·ai作画
jerrywus10 小时前
让 AI 写完代码后交一份报告:我改造了 answer-me-with-html
前端·claude·cursor
派小心.11 小时前
小程序分享链路中来源参数的传递校验
大数据·前端·数据库·人工智能·小程序·数据分析
hhb_61811 小时前
一键生成数据看板,ECharts实战演示
前端·javascript·echarts
Java后端的Ai之路11 小时前
08_React组件API
前端·react.js·前端框架
小马不起床12 小时前
零基础玩转 FastAPI_09:前端基础工程构建
前端·状态模式·fastapi
风骏时光牛马13 小时前
用户管理模块接口开发
前端