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

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

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

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

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

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 相当于"把这个类型从联合里过滤掉"。所以 ExcludeExtractNonNullable 都是基于条件类型+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 结果等)------这是 ReturnTypePromiseType 等高级工具的核心。

4. 要点总结

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

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

相关推荐
涛涛ing1 小时前
一周之内,前端 AI 编程的格局被彻底改写了三次
前端·程序员
fxss1 小时前
免费儿童学习网站
前端
Eiceblue1 小时前
如何构建一个纯前端 Word 转 Markdown 转换器(React + WebAssembly)
前端·react.js·word
平头哥~2 小时前
Day 15 | 不改一行 HTML,给页面加上引号、角标和标签
前端·javascript·css·html·css3·学习资料
风骏时光牛马2 小时前
后端系统性能监控与异常告警管理
前端
Fluxart.ai3 小时前
GPT Image 2 国内怎么用?在 Flux Art 完成图片生成与编辑
前端·javascript·gpt
tanzongbiao3 小时前
Dorado7文件上传 解决HttpServletResponse返回值中文乱码的问题
java·服务器·前端
郑州光合科技余经理7 小时前
本地生活服务系统:成品模块和定制接口怎么划界
java·前端·人工智能·后端·系统架构·php·ai编程
平头哥~10 小时前
Day 07 _ 那条 1px 的线,为什么在手机上忽粗忽细
前端·css·样式·学习资料