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

基于id和label属性扁平化对象的TypeScript严格类型实现问题

TypeScript对象扁平化的类型约束实现

需求

需要将如下结构的对象:

{
  article: 'prova',
  id: 63,
  topology: { id: 'topId', label: 'topLabel' },
  something: { id: 'someId', label: 'someLabel' }
}

扁平化为:

{
  article: "prova",
  id: 63,
  topId: "topLabel",
  someId: "someLabel"
}

类型约束要求

输入和输出必须满足严格的TypeScript类型约束,示例代码如下:

interface Input {
  article: string
  id: number
  abc: { id: string; label: string }
  def: { id: string; label: string }
}
interface Output {
  article: string
  id: number
  topId: string
  someId: string
}
const input1: Input = {
  article: 'prova',
  id: 63,
  abc: { id: 'topId', label: 'topLabel' },
  def: { id: 'someId', label: 'someLabel' }
}
const input2 = {
  article: 'prova',
  id: 63,
  abc: { id: 'topId', label: 'topLabel' },
  def: { id: 'someId', label: 'someLabel' }
}
// 报错信息:
// Type 'Flattened<string | number | null | undefined, { id: string; label: string; }, Record<string, string | number | { id: string; label: string; } | null | undefined>>' is missing the following properties from type 'Output': article, id, topId, someId
// Argument of type 'Input' is not assignable to parameter of type 'Record<string, string | number | { id: string; label: string; } | null | undefined>'.
// Index signature for type 'string' is missing in type 'Input'.
const output1: Output = flattenObject(input1)
// 报错信息:
// Type 'Flattened<string | number | null | undefined, { id: string; label: string; }, { article: string; id: number; abc: { id: string; label: string; }; def: { id: string; label: string; }; }>' is missing the following properties from type 'Output': topId, someId
const output2: Output = flattenObject(input2)

我的尝试实现(存在类型错误)

我写了以下代码,但TypeScript提示上述类型错误:

function hasId<
  K extends string | number | undefined | null,
  T extends { id: string; label: string }
>(el: [string, K | T]): el is [string, T] {
  return (el[1] as T).id != null
}

type Flattened<
  K extends string | number | undefined | null,
  T extends { id: string; label: string },
  S extends Record<string, K | T>
> = {
  [key in keyof S as S[key] extends T ? S[key]['id'] : key]: S[key] extends T
    ? S[key]['label']
    : S[key]
}

const flattenObject = <
  K extends string | number | undefined | null,
  T extends { id: string; label: string },
  S extends Record<string, K | T>
>(
  obj: S
): Flattened<K, T, S> =>
  Object.fromEntries(
    Object.entries(obj).map<[string, K | string]>((el) =>
      hasId(el) ? [el[1].id, el[1].label] : (el as [string, K])
    )
  ) as Flattened<K, T, S>

解决方案

问题出在几个核心点:

  1. S extends Record<string, ...>要求输入对象必须有字符串索引签名,但Input接口没有,导致类型不兼容。
  2. Flattened类型中直接引用S[key]['id']是值层面的操作,无法作为静态类型的键名,需要用条件类型提取类型信息。

以下是修正后的实现:

// 类型守卫:判断值是否为带id和label的对象
function isLabeledObject(value: unknown): value is { id: string; label: string } {
  return typeof value === 'object' && value !== null && 'id' in value && 'label' in value;
}

// 定义扁平化后的类型:
// - 原对象中符合{id: string; label: string}的属性,替换为[id的静态类型: label的类型]
// - 其他属性保持原键值对
type FlattenedObject<Obj> = {
  [K in keyof Obj as Obj[K] extends { id: infer Id extends string; label: infer _ } 
    ? Id 
    : K]: Obj[K] extends { id: infer _; label: infer Label } 
      ? Label 
      : Obj[K]
};

function flattenObject<Obj extends Record<string, unknown>>(obj: Obj): FlattenedObject<Obj> {
  return Object.fromEntries(
    Object.entries(obj).map(([key, value]) => {
      if (isLabeledObject(value)) {
        return [value.id, value.label];
      }
      return [key, value];
    })
  ) as FlattenedObject<Obj>;
}

// 测试验证
interface Input {
  article: string
  id: number
  abc: { id: 'topId'; label: string }
  def: { id: 'someId'; label: string }
}
interface Output {
  article: string
  id: number
  topId: string
  someId: string
}

const input1: Input = {
  article: 'prova',
  id: 63,
  abc: { id: 'topId', label: 'topLabel' },
  def: { id: 'someId', label: 'someLabel' }
};

const output1: Output = flattenObject(input1); // 类型匹配,无报错
const output2 = flattenObject({
  article: 'prova',
  id: 63,
  abc: { id: 'topId', label: 'topLabel' },
  def: { id: 'someId', label: 'someLabel' }
});
// output2类型自动推导为:{ article: string; id: number; topId: string; someId: string }

关键修正说明

  • 用Obj extends Record<string, unknown>替代原有约束,兼容所有对象类型(包括无索引签名的接口)。
  • 通过infer关键字提取id的静态类型作为新键名,确保类型推导的准确性。
  • 简化并严谨化类型守卫函数,避免嵌套泛型带来的类型复杂度。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 21:14:55