Refine 与 TypeScript Omit 类型:掌握对象类型裁剪,写出更精简的代码
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
导读
Omit<Type, Keys>是 TypeScript 内置的实用类型之一,它允许你从既有类型中移除指定的属性,从而派生出新的类型。本文以本仓库(refine,一个用于构建内部工具、后台面板与 B2B 应用的 React 框架)的官方技术博客为蓝本,通过Subscriber、PublicUser、FrontendUser等完整示例讲解Omit<>的用法,并深入 refine 各包源码,展示Omit<>在真实框架代码中如何被用来裁剪 react-query 选项、Pagination 与按钮组件 props。读完本文,你将掌握Omit<>的语法、与Pick<>的取舍、编译期与运行时的差异,以及如何在日常开发与开源项目维护中用它精简类型。
什么是 TypeScriptOmit<Type, Keys>
TL;DR:Omit<Type, Keys>通过从既有类型中删除某些属性来构造一个新类型。当某些属性不需要时,它是减少冗余代码的绝佳工具。
与Pick<>类似,Omit<>接受两个参数:
- 第一个参数是基础类型(
Type); - 第二个参数是要省略的属性键的联合类型(
Keys,多个键用|分隔); - 返回值是排除了这些属性后的派生类型。
本篇文章属于《TypeScript Utility Type Series》系列的第二篇,前一篇《A Guide on TypeScript Pick Type》见 documentation/blog/2024-12-17-typescript-pick.md。上一篇中我们用Pick<>从基础类型SuperbUser中挑选少量属性派生出GuestUser,并提示:当需要保留的属性数量多于需要丢弃的属性数量时,Pick<>反而不太方便,此时应优先考虑Omit<>。本篇我们就围绕这一点,用Omit<>为Subscriber(订阅者)实体创建类型。
TypeScriptOmit<Type, Keys>实战示例
假设我们有一张用户关系表,其中SuperbUser实体拥有以下属性:
type SuperbUser = { userId: number; macAddress: string; username: string; email: string; password: string; firstName: string; lastName: string; roles: ("Admin" | "Editor" | "Author")[]; };而Subscriber实体拥有与SuperbUser完全相同的属性——唯独没有roles。如果使用Pick<>,我们需要把除了roles之外的所有属性逐一列出;而使用Omit<>只需要指定要删除的那一个键:
type Subscriber = Omit<SuperbUser, "roles">;这种情况下,Omit<>比Pick<>更方便。现在我们可以创建一个不再包含roles字段的订阅者对象:
const subscriber: Subscriber = { userId: 4, macAddress: "a:5ub:mach1ne", username: "sub_user", email: "sub_user@gmail.com", password: "12345678", firstName: "Abdullah", lastName: "Numan", }; console.log(subscriber); /* { "userId": 4, "macAddress": "a:5ub:mach1ne", "username": "sub_user", "email": "sub_user@gmail.com", "password": "12345678", "firstName": "Abdullah", "lastName": "Numan" } */ console.log(subscriber.roles); // undefined注意:subscriber.roles在类型层面已被移除,访问它返回undefined。如果我们强行在开发期给它赋值:
subscriber.roles = ["Reader", "Commenter"]; // Property 'roles' does not exist on type 'Subscriber'.TypeScript 会立刻报错:
// Property 'roles' does not exist on type 'Subscriber'.但有趣的是,如果我们此时再看console.log(subscriber.roles),会发现这个赋值实际上在运行时真的生效了。也就是说,Omit<>只是开发期的类型约束,TypeScript 并不会在编译成 JavaScript 之后继续检查代码的任何后果。JavaScript 引擎会照常把subscriber.roles的值设置进去。
重要提醒:
Omit<>是开发期(编译期)的工具,不是运行时防护。务必结合 linter(如@typescript-eslint)与类型检查来严格执行 omit 约束,防止敏感字段意外暴露。
TypeScriptOmit<>与 Interface
与Pick<>一样,基础类型也可以是interface,结果完全相同:
interface SuperbUser { userId: number; macAddress: string; username: string; email: string; password: string; firstName: string; lastName: string; roles: ("Admin" | "Editor" | "Author")[]; } type Subscriber = Omit<SuperbUser, "roles">; const subscriber: Subscriber = { userId: 4, macAddress: "a:5ub:mach1ne", username: "sub_user", email: "sub_user@gmail.com", password: "12345678", firstName: "Abdullah", lastName: "Numan", }; console.log(subscriber); /* { "userId": 4, "macAddress": "a:5ub:mach1ne", "username": "sub_user", "email": "sub_user@gmail.com", "password": "12345678", "firstName": "Abdullah", "lastName": "Numan" } */ console.log(subscriber.roles); // undefined无论基础类型是type还是interface,Omit<>的第二个参数都接受属性键的联合类型,多个键用管道符|连接:
type Subscriber = Omit<SuperbUser, 'roles' | 'firstName' | ...>;何时应该避免使用Omit<>
与Pick<>相对的取舍规则同样成立:当需要省略的属性多于需要保留的属性时,应避免使用Omit<>,转而使用Pick<>。
例如:如果SuperbUser有 8 个属性,而你只需要其中的 2 个,那么Omit<>需要列出 6 个要删除的键,远不如Pick<>列出 2 个要保留的键来得直观、不易出错。选择标准很简单——保留的少用Pick<>,丢弃的少用Omit<>。
何时应该使用 TypeScriptOmit<>
Omit<>的核心价值在于:保留大部分字段、只剔除少数几个。典型场景包括:
- 移除敏感字段:例如从用户对象中剔除
password、passwordHash、createdAt等; - 为复杂类型生成简化版本:基础类型很庞大,但某些场景只需要其中绝大部分字段;
- 快速适配第三方类型:不想手写一个全新的类型,只想对现有类型做微调。
下面逐一展开。
简化派生类型
当你有一个复杂的基础类型,但需要一个去掉少数字段的简化版本时,Omit<>非常方便:
type FullUser = { id: number; name: string; email: string; password: string; createdAt: Date; }; // 创建一个不含敏感数据(password、createdAt)的 PublicUser 类型 type PublicUser = Omit<FullUser, "password" | "createdAt">; const user: PublicUser = { id: 1, name: "John Doe", email: "johndoe@gmail.com", }; console.log(user); /* Output: { id: 1, name: "John Doe", email: "johndoe@gmail.com" } */API 数据过滤
有时候 API 或后端会返回完整的对象,而前端只需要其中一小部分字段。你可以手写一个全新的类型,也可以直接用Omit<>快速裁剪:
interface ApiResponse { id: number; username: string; email: string; passwordHash: string; isAdmin: boolean; } // 创建一个不含敏感后端数据的 FrontendUser 类型 type FrontendUser = Omit<ApiResponse, "passwordHash" | "isAdmin">; const frontendUser: FrontendUser = { id: 101, username: "frontend_dev", email: "dev@example.com", };为特定上下文创建更干净的类型
当你在处理表单、UI 组件或其他模块时,往往只需要父类型中的部分字段。Omit<>能让你的类型保持整洁和聚焦:
interface FullProduct { id: string; name: string; description: string; price: number; createdAt: Date; updatedAt: Date; } // 为 UI 表单创建不含元数据的 ProductForm 类型 type ProductForm = Omit<FullProduct, "id" | "createdAt" | "updatedAt">; const formData: ProductForm = { name: "Gaming Laptop", description: "A powerful laptop for gaming.", price: 1500, };Pick与Omit对比
| 特性 | Pick | Omit |
|---|---|---|
| 用途 | 选择特定字段 | 排除特定字段 |
| 语法 | Pick<Type, Keys> | Omit<Type, Keys> |
| 适用场景 | 只需要少量字段时 | 只需要省略少量字段时 |
| 示例 | type A = Pick<Type, "id" \| "name">; | type B = Omit<Type, "password">; |
| 结果 | 仅包含id和name | 排除password |
一句话概括:Omit<>是Pick<>的“相反等价物”,当我们要保留更多、省略更少时,Omit<>更便捷。
从源码看Omit<>在 refine 中的真实应用
Omit<>不只是教程里的玩具,它在 refine 这类大型开源框架的源码中无处不在。下面几个例子都来自当前仓库,可以帮你直观理解它在真实工程中的价值。
裁剪 react-query 的useQuery选项
在 packages/core/src/hooks/data/useOne.ts#L50-L69 中,useOnehook 的queryOptions是这样定义的:
queryOptions?: Omit< UseQueryOptions< GetOneResponse<TQueryFnData>, TError, GetOneResponse<TData> >, "queryKey" | "queryFn" > & { // Make queryKey and queryFn optional queryKey?: UseQueryOptions< GetOneResponse<TQueryFnData>, TError, GetOneResponse<TData> >["queryKey"]; queryFn?: UseQueryOptions< GetOneResponse<TQueryFnData>, TError, GetOneResponse<TData> >["queryFn"]; };这里 refine 用Omit<UseQueryOptions<...>, "queryKey" | "queryFn">从 TanStack Query 的UseQueryOptions类型中剔除queryKey与queryFn(因为这两个值由 hook 内部根据resource、id等参数自动计算),随后再通过交叉类型把这两个字段以可选形式放回去。这是"继承第三方类型、约束内部字段"的经典写法。
裁剪useMutation选项
同理,在 packages/core/src/hooks/data/useCreate.ts#L79-L87 中,useCreate的mutationOptions通过Omit<UseMutationOptions<...>, "mutationFn">把 TanStack Query 的mutationFn移除(它由 refine 内部绑定到dataProvider.create),只允许调用方传入其余配置:
mutationOptions?: Omit< UseMutationOptions< CreateResponse<TData>, TError, UseCreateParams<TData, TError, TVariables>, unknown >, "mutationFn" >;useOne、useCreate所在的数据 hooks 目录见 packages/core/src/hooks/data,类似的Omit<>用法还出现在useUpdate、useDelete、useCustom、useInfiniteList等多个 hooks 中。
改写Pagination的mode字段
在 packages/core/src/hooks/useSelect/index.ts#L111-L119 中,useSelect的pagination参数被定义为:
pagination?: Prettify< Omit<Pagination, "mode"> & { /** * Whether to use server side pagination or not. * @default "off" */ mode?: Pagination["mode"]; } >;refine 先用Omit<Pagination, "mode">去掉Pagination类型里的mode字段,再把它重新声明为可选并附上文档注释。这样既保留了原有字段,又让内部受控的字段对外暴露为可选项——Omit<>在此扮演了"类型重构"的角色。
精简按钮组件的 props
在 packages/chakra-ui/src/components/buttons/types.ts#L17-L25 中,Chakra UI 适配层的按钮类型也大量使用Omit<>:
export type ShowButtonProps = Omit< RefineShowButtonProps< ButtonProps, { svgIconProps?: Omit<IconProps, "ref">; } >, "ignoreAccessControlProvider" >;这里有两层Omit:
- 外层
Omit<RefineShowButtonProps<...>, "ignoreAccessControlProvider">从 UI 类型定义中移除框架内部使用的ignoreAccessControlProvider字段,避免暴露给外部使用者; - 内层
Omit<IconProps, "ref">从 Tabler Icons 的IconProps中剔除ref,防止 SVG 图标属性被传入不合法的 ref。
这展示了Omit<>在框架封装层的另一个典型用途:隐藏内部实现细节,收紧公开 API 的类型面。
编译期 vs 运行时:为什么 linter 很重要
从上面的源码示例可以再次确认:Omit<>的一切约束都发生在编译期。TypeScript 编译成 JavaScript 后,类型信息会被完全擦除,Omit不会产生任何运行时代码。因此:
- 类型层面的"删除"不等于运行时的"删除"——像本文开头
subscriber.roles = [...]这样的赋值在运行时依然有效; - 若要在运行时真正剔除敏感字段,需要配合
destructuring、pick/omit之类的工具函数或序列化逻辑; - 开发期要依靠 TypeScript 编译检查与 linter 规则(例如
@typescript-eslint的相关配置)来确保 omit 约束不被绕过。
这也是 refine 在源码中大量使用Omit<>做 API 面收紧的底气:类型错误可以在 CI/编辑器里第一时间暴露,而不会等到运行时才发现字段泄漏。
结语
本文围绕Subscriber实体的派生,完整演示了Omit<Type, Keys>的用法:它接受基础类型与待删除键的联合类型,返回剔除这些属性后的新类型;基础类型可以是type也可以是interface;当需要省略的属性多于保留的属性时应改用Pick<>;敏感字段剔除、API 数据过滤、UI 表单类型精简都是它的经典应用场景。随后我们通过 refine 源码中的useOne、useCreate、useSelect与 Chakra UI 按钮类型,看到了它在真实框架中"裁剪第三方类型、隐藏内部字段、收紧公开 API"的实战价值。
最后务必牢记:Omit<>是编译期工具而非运行时防护,Pick<>与Omit<>是一对互补的伙伴——保留少用Pick,省略少用Omit。本系列的下一篇将介绍利用Partial<Type>进行对象类型转换。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考