Skip to content

keyBy

将数组转为以某字段为 key 的对象(常用于快速查找)。

  • 支持字符串字段(包含点路径如 user.id)或选择器函数。
  • 支持重复键保留策略:first(保留首次出现)/last(保留最后一次,默认)。
  • 支持 symbol 作为键;当键为 null/undefined 时可选择跳过。

安装与导入

  • 子路径导入:import keyBy from '@cuixingjian/cui-utils/keyBy'
  • 命名导入:import { keyBy } from '@cuixingjian/cui-utils'

使用示例

ts
import { keyBy } from '@cuixingjian/cui-utils'

// 基于简单字段
const users = [
  { id: 1, name: 'A' },
  { id: 2, name: 'B' },
]
const byId = keyBy(users, 'id')
// => { 1: { id: 1, name: 'A' }, 2: { id: 2, name: 'B' } }

// 点路径字段
const data = [
  { user: { id: 'u1' }, role: 'admin' },
  { user: { id: 'u2' }, role: 'user' },
]
const byUserId = keyBy(data, 'user.id')
// => { u1: { user: { id: 'u1' }, role: 'admin' }, u2: { ... } }

// 使用选择器函数(返回 string/number/symbol)
const orders = [
  { no: '001', items: 3 },
  { no: '002', items: 5 },
]
const byNo = keyBy(orders, o => o.no)

// 处理重复键:保留首次出现
const dup = [
  { id: 1, name: 'first' },
  { id: 1, name: 'last' },
]
const firstOnly = keyBy(dup, 'id', { keep: 'first' })
// => { 1: { id: 1, name: 'first' } }

// 跳过空键(null/undefined)
const arr = [
  { id: 1 },
  { id: null },
  { id: undefined },
]
const res = keyBy(arr, 'id', { skipEmptyKey: true })
// => { 1: { id: 1 } }

API

函数签名

ts
function keyBy<T>(
  list: T[],
  selector: string | ((item: T) => PropertyKey),
  options?: KeyByOptions
): Record<PropertyKey, T>

参数

参数名类型必填说明
listT[]源数组
selector`string(item: T) => PropertyKey`
optionsKeyByOptions选项配置,见下表。

KeyByOptions

名称类型默认值说明
keep`'first''last'`'last'
skipEmptyKeybooleantrue当生成的键为 null/undefined 时跳过该项。若设为 false,将以字符串 'undefined' 作为键进行归集。

返回值

类型说明
Record<PropertyKey, T>键为 string/number/symbol 的对象映射,值为数组项。

注意事项

  • selector 返回非 string/number/symbol 类型时会自动转为字符串作为键。
  • skipEmptyKeyfalse 且键为 undefined 时,使用字符串 'undefined' 作为键。
  • 使用函数选择器可直接返回 symbol 作为键,便于与外部 Symbol 常量映射。

源码

ts
import keyBy from '@cuixingjian/cui-utils/keyBy'
展开查看源码
ts
export type KeyBySelector<T> = (item: T) => PropertyKey

export interface KeyByOptions {
  keep?: 'first' | 'last'
  skipEmptyKey?: boolean
}

function getByPath(obj: any, path: string): unknown {
  const parts = path.split('.')
  let cur = obj
  for (const p of parts) {
    if (cur == null) return undefined
    cur = cur[p]
  }
  return cur
}

export function keyBy<T extends Record<string | number | symbol, any>>(
  list: T[],
  selector: string | KeyBySelector<T>,
  options: KeyByOptions = {}
): Record<PropertyKey, T> {
  const { keep = 'last', skipEmptyKey = true } = options
  const out: Record<PropertyKey, T> = {}

  const makeKey = (item: T): PropertyKey | undefined => {
    if (typeof selector === 'function') {
      const k = selector(item)
      return k as PropertyKey
    }
    const val = getByPath(item, selector)
    if (val === null || val === undefined) return undefined
    const t = typeof val
    if (t === 'string' || t === 'number' || t === 'symbol') return val as PropertyKey
    return String(val) as PropertyKey
  }

  for (const it of list) {
    const key = makeKey(it)
    if (key === undefined && skipEmptyKey) continue
    if (key === undefined) {
      const k: PropertyKey = 'undefined'
      if (keep === 'first') {
        if (!(k in out)) Reflect.set(out, k, it)
      } else {
        Reflect.set(out, k, it)
      }
      continue
    }
    if (keep === 'first') {
      if (!(key in out)) Reflect.set(out, key, it)
    } else {
      Reflect.set(out, key, it)
    }
  }

  return out
}

export default keyBy