Skip to content

omitpick:对象属性的精确裁剪

在 JavaScript 开发中,我们经常需要对对象进行“过滤”操作:

  • 移除某些敏感或无用的字段(如 password)。
  • 提取特定子集用于 API 请求或组件 props。

omitpick 正是为此而生——它们提供了一种声明式、不可变的方式,安全地裁剪对象结构。

更重要的是,在 TypeScript 环境下,它们能通过高级类型系统,实现返回类型的自动推导,让编辑器成为你的实时助手。

omit:移除指定键,返回新对象

核心语义

从对象中排除一组指定的键,返回一个不包含这些键的新对象。

ts
const user = { id: 1, name: 'Alice', password: '123456' }
const safeUser = omit(user, ['password'])
// safeUser: { id: 1, name: 'Alice' }

实现要点

1. 不可变性(Immutability)

绝不修改原对象,始终返回新引用:

ts
const omit = <T extends object, K extends keyof T>(
  obj: T,
  keys: K[]
): Omit<T, K> => {
  const result = {} as T
  for (const key in obj) {
    if (!keys.includes(key)) {
      result[key] = obj[key]
    }
  }
  return result
}

2. 使用 Object.keys + filter + reduce

更函数式的写法:

ts
const omit = <T extends object, K extends keyof T>(
  obj: T,
  keys: K[]
): Omit<T, K> => {
  return Object.keys(obj)
    .filter((key) => !keys.includes(key as K))
    .reduce((result, key) => {
      result[key] = obj[key]
      return result
    }, {} as any)
}

注意reduce 初始值为 {},类型需断言或通过泛型约束避免错误。

pick:保留指定键,返回新对象

核心语义

只保留对象中指定的键,其余全部丢弃。

ts
const user = { id: 1, name: 'Alice', email: 'alice@example.com' }
const userInfo = pick(user, ['name', 'email'])
// userInfo: { name: 'Alice', email: 'alice@example.com' }

实现

ts
const pick = <T extends object, K extends keyof T>(
  obj: T,
  keys: K[]
): Pick<T, K> => {
  return keys.reduce((result, key) => {
    if (key in obj) {
      result[key] = obj[key]
    }
    return result
  }, {} as Pick<T, K>)
}

关键点

  • 只遍历 keys 数组,而非整个对象,性能更优(尤其当对象很大时)。
  • 检查 key in obj 防止访问不存在的属性(尽管 obj 类型已约束,但运行时仍可能缺失)。

不可变性:为何必须返回新引用?

JavaScript 中的对象是引用类型。若直接修改原对象:

ts
delete user.password // ❌ 破坏原数据,可能导致其他模块异常

omitpick 遵循不可变性原则

  • 原对象保持完整,可用于日志、缓存等。
  • 新对象独立存在,可自由传递、修改。
  • 符合函数式编程理念,减少副作用。

这在 React、Vue 等响应式框架中尤为重要——状态变更必须通过新引用来触发更新。

类型推导:让返回类型“智能”排除或保留键

最强大的不是逻辑实现,而是类型系统的精确描述

目标

调用 omit(user, ['password']) 后,返回类型应自动变为:

ts
{ id: number; name: string } // 即排除了 'password'

解法:使用内置工具类型

TypeScript 提供了 Omit<T, K>Pick<T, K>

ts
type Omit<T, K extends keyof any> = {
  [P in Exclude<keyof T, K>]: T[P]
}

type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

在函数中应用

ts
const omit = <T extends object, K extends keyof T>(
  obj: T,
  keys: K[]
): Omit<T, K> => { /* ... */ }

const pick = <T extends object, K extends keyof T>(
  obj: T,
  keys: K[]
): Pick<T, K> => { /* ... */ }

类型推导示例

text
type User = { id: number; name: string; password: string }

const user: User = { id: 1, name: 'Alice', password: '123' }

const safeUser = omit(user, ['password'])
// safeUser 的类型被推导为: { id: number; name: string }

const nameOnly = pick(user, ['name'])
// nameOnly 的类型被推导为: { name: string }

IDE 能立即识别 safeUser.password 不存在,提供精准的错误提示。

映射类型深度解析:Omit 如何工作?

Omit<T, K> 的核心是映射类型(Mapped Types)与条件类型结合:

ts
{
  [P in Exclude<keyof T, K>]: T[P]
}

分解:

  1. keyof T:获取 T 的所有键,如 'id' | 'name' | 'password'
  2. Exclude<keyof T, K>:从 keyof T 中排除 K 的键。
    • K'password',则结果为 'id' | 'name'
  3. [P in ...]:遍历剩余键。
  4. T[P]:取原对象对应属性的类型。

最终生成一个动态构造的新类型,完美匹配运行时行为。

实战:在 API 请求中安全发送数据

ts
// 用户登录后返回的完整用户信息
interface FullUser {
  id: number
  name: string
  email: string
  password: string // 敏感字段
  createdAt: string
}

// 前端展示用的简化用户
type DisplayUser = Omit<FullUser, 'password' | 'createdAt'>

const currentUser: FullUser = await login(username, password)

// 安全发送到前端组件
const displayUser: DisplayUser = omit(currentUser, ['password', 'createdAt'])

// 或用于 PATCH 请求,只更新部分字段
const updatePayload = pick(formData, ['name', 'email'])
await api.patch(`/users/${id}`, updatePayload)

结语:类型即文档,行为即契约

omitpick 看似简单,却体现了现代前端开发的核心范式:

  • 不可变性:数据流清晰可追溯。
  • 函数式:纯函数,无副作用。
  • 类型驱动:返回类型由输入参数精确决定。

当你手写 omit 时,你不仅在实现一个工具函数,更在践行一种工程哲学:
让类型系统成为程序逻辑的精确镜像,让每一次调用都是一次安全的、可推理的契约履行