找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

983

积分

0

好友

139

主题
发表于 3 天前 | 查看: 14| 回复: 0

许多前端开发者在编写 TypeScript,特别是接触到深层泛型嵌套或阅读开源库源码时,看到类似 T extends U ? X : Y 这样的条件类型四处散落,往往会感到困惑。如果再结合 infer 关键字,理解难度更是急剧上升。

这种对类型系统进行复杂操作和推导的技巧,通常被称为类型体操。本文不空谈概念,而是通过十个具体的实战场景,帮助你透彻理解条件类型与 infer 的协同工作原理。

核心模式速记

在深入场景之前,请务必掌握这个核心模式公式:

type MyType<T> = T extends SomeType<infer R> ? R : never;

你可以将其理解为:检查类型 T 是否匹配 SomeType<某部分> 这种结构。如果匹配,就将其中的“某部分”推断出来并命名为 R 以供使用。

一、 类型拆解:提取嵌套结构中的内部类型

这是最基础且最高频的应用场景,旨在从层层包裹的类型中提取出我们需要的部分。

场景 1:提取 Promise 的返回值类型

当你拥有一个 Promise<X> 类型,但只需要内部的 X 类型时,可以这样操作:

// 原始类型
type UserPromise = Promise<{ id: number; name: string }>;

// 拆解类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type User = UnwrapPromise<UserPromise>;
// 结果:User 的类型为 { id: number; name: string }
场景 2:获取数组元素的类型

从一个数组类型(如 User[])中提取其单个元素的类型,常用于定义详情组件的 Props。

type ArrayElement<T> = T extends (infer U)[] ? U : never;

type Item = ArrayElement<string[]>; // Item 的类型为 string
场景 3:深层递归解包 Promise

对于 Promise<Promise<string>> 这类多层嵌套的 Promise,上述 UnwrapPromise 只能解开一层。此时需要引入递归。

// 注意后续的递归调用 UnwrapDeep<U>
type UnwrapDeep<T> = T extends Promise<infer U> ? UnwrapDeep<U> : T;

type Result = UnwrapDeep<Promise<Promise<string>>>; // Result 的类型为 string

二、 应用于函数类型推导

在封装工具函数、高阶组件或第三方库时,经常需要提取原函数的参数类型或返回值类型。

场景 4:获取函数返回值类型(实现 ReturnType)

这是 TypeScript 内置的工具类型,理解其实现原理至关重要。

const getUser = () => ({ name: 'Jack', age: 18 });

// 注意 infer 放置的位置
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type User = GetReturnType<typeof getUser>;
// 结果:User 的类型为 { name: string; age: number }
场景 5:提取函数的第一个参数类型

在组件封装中,有时只需要透传事件处理函数的第一个参数(如 event 对象)。

type FirstArg<T> = T extends (first: infer F, ...args: any[]) => any ? F : never;

type Handler = (e: Event, id: string) => void;
type EventType = FirstArg<Handler>; // EventType 的类型为 Event

三、 结合模板字面量类型

TypeScript 4.1 引入的模板字面量类型,与 infer 配合后,成为了现代前端框架(如 Next.js、TanStack Router)实现类型安全路由的关键技术。

场景 6:提取字符串最后一部分

处理全名或文件路径,仅保留最后一段。

type GetLastName<T extends string> =
  T extends `${infer _FirstWord} ${infer RestOfString}`
    // 注意这里的递归调用
    ? GetLastName<RestOfString>
    : T;

type Name = GetLastName<"John Von Neumann">; // Name 的类型为 "Neumann"
场景 7:移除字符串前缀

在处理类似 Vue 或 React 中 onClick 这类 Props 命名,需要将其转换为内部事件名时很有用。

type RemoveOnPrefix<T> = T extends `on-${infer Rest}` ? Rest : T;

type EventName = RemoveOnPrefix<"on-click">; // EventName 的类型为 "click"
场景 8:解析路由参数(高阶应用)

这是一个非常强大的场景:从形如 /user/:id/post/:postId 的路由字符串中,自动推导出参数对象的类型。

type ExtractRouteParams<Path extends string> =
  Path extends `${infer _Prefix}/:${infer ParamName}/${infer Rest}`
    ? ParamName | ExtractRouteParams<Rest>
    : Path extends `${infer _Prefix}/:${infer ParamName}`
    ? ParamName
    : never;

type RouteParamsObject<Keys extends string, ValueType = string> = {
  [Key in Keys]: ValueType;
};

type GetRouteParamsType<Path extends string> = RouteParamsObject<
  ExtractRouteParams<Path>
>;

type RouteParams = GetRouteParamsType<'/users/:id/posts/:postId/edit'>;
// 结果:RouteParams 的类型为 { id: string; postId: string; }

四、 关键注意事项与避坑指南

条件类型有两个需要特别注意的特性:分发(Distributive Conditional Types) 和潜在的递归死循环

场景 9:如何避免联合类型的自动分发

这是常见的困惑点。当 T 是联合类型(如 string | number)时,T extends U 会将其拆分并分别进行条件判断,最后合并结果。

type ToArray<T> = T extends any ? T[] : never;

type Result = ToArray<string | number>;
// 结果是:string[] | number[] (发生了分发)
// 但我们期望的可能是:(string | number)[]

解决方案:如果希望将联合类型作为一个整体处理,可以用方括号将其包裹。

// ✅ 修正版本,阻止分发
type ToArraySafe<T> = [T] extends [any] ? T[] : never;

type ResultSafe = ToArraySafe<string | number>; // 结果为 (string | number)[]
场景 10:过滤掉 null 和 undefined(实现 NonNullable)

当泛型 T 中可能包含 nullundefined 时,可以使用条件类型将其排除。

// 原理:如果 T 是 null 或 undefined,则返回 never。在联合类型中,never 会被自动忽略。
type MyNonNullable<T> = T extends null | undefined ? never : T;

type Clean = MyNonNullable<string | null | undefined>; // Clean 的类型为 string

总结与实践建议

掌握以上十个场景后,你或许已经跃跃欲试,想要重构项目中的类型定义了。但需要清醒地认识到:类型体操是一把双刃剑

在开发通用工具库、公共组件或框架时,娴熟运用这些技巧能极大提升代码的类型安全性和开发者体验。然而,在复杂的业务逻辑代码中,过度使用晦涩的类型体操可能会显著降低代码的可读性和可维护性,为团队协作带来障碍。

牢记最核心的两点:extends 用于条件判断,infer 用于在条件分支中声明一个类型变量。把握住这个本质,你就能读懂和编写大多数“类型体操”了。在实际开发中,务必在类型表达力的强大与代码的简洁清晰之间找到平衡。




上一篇:后端开发2023秋招亲历:大厂面试复盘与寒冬下的求职思考
下一篇:Spring Boot列表接口设计:分页封装规范与扩展字段最佳实践
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 20:13 , Processed in 0.152098 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表