You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TypeScript实现arrayToTree如何无需@ts-ignore解决类型报错

无@ts-ignore的最终实现

// 定义带kids属性的树节点类型,子节点也保持相同结构
type WithKids<T> = T & { kids: WithKids<T>[] }

// 声明三个泛型:
// T: 原始数组项的类型,约束其itemIdKey、parentIdKey对应的值为 字符串/数字/空值
// ID: itemIdKey的字面量类型
// PID: parentIdKey的字面量类型
const arrayToTree = <
  T extends Record<ID | PID, string | number | null | undefined>,
  ID extends string,
  PID extends string
>(
  array: T[],
  { itemIdKey, parentIdKey }: { itemIdKey: ID; parentIdKey: PID }
): WithKids<T>[] => {
  if (itemIdKey === parentIdKey) {
    throw new Error('itemIdKey and parentIdKey has to be different')
  }

  const map: Record<string | number, WithKids<T>> = {}
  const roots: WithKids<T>[] = []

  // 第一次遍历生成所有带kids属性的节点存到map
  array.forEach(item => {
    const id = item[itemIdKey]
    // 运行时校验id合法性,避免非基础类型作为索引
    if (typeof id !== 'string' && typeof id !== 'number') {
      throw new Error(`itemId value must be string or number, got ${typeof id}`)
    }
    map[id] = {
      ...item,
      kids: []
    }
  })

  // 第二次遍历挂载节点
  array.forEach(item => {
    const parentId = item[parentIdKey]
    const currentNode = map[item[itemIdKey] as string | number]
    // 父id为空、为'0'、或者父id不存在于map时,都作为根节点
    if (!parentId || parentId === '0' || !map[parentId]) {
      roots.push(currentNode)
    } else {
      map[parentId].kids.push(currentNode)
    }
  })

  return roots
}

核心改动说明

  • 新增泛型约束:明确原始数组项T的itemIdKey、parentIdKey对应的属性值只能是字符串/数字/空值,TS就可以确认这两个值可以作为对象索引,解决索引类型不兼容的报错
  • 拆分泛型为T、ID、PID三个,捕获key的具体字面量类型,TS可以自动窄化对应属性的类型,解决和字符串'0'比较的类型报错
  • 修正原逻辑缺陷:原来挂载子节点时直接push原始数组项,改为push map中已生成的带kids属性的节点,支持多层嵌套
  • 新增运行时校验:避免传入非字符串/数字类型的id导致运行异常
  • 新增父节点不存在的兼容处理:如果父id在map中找不到对应节点,直接作为根节点,避免运行时报错

内容的提问来源于stack exchange,提问作者Ni Amon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.27 09:18:00