【问题标题】:How can an object type with nested subproperties be flattened?如何扁平化具有嵌套子属性的对象类型?
【发布时间】:2021-12-17 06:30:49
【问题描述】:

我有下一个数据类型为 Data

type Data = {
  Id: string,
  LogicalName: string,
  VATRegistered: {
    Label: string | null,
    Value: number | null,
  }
}

const data: Data = {
  Id: 'qK1jd828Qkdlqlsz8123assaa',
  LogicalName: 'locale',
  VATRegistered: {
    Label: 'AT401',
    Value: 1000001
  }
}

我必须将其转换为下一个:

const transformedData = {
  Id: 'qK1jd828Qkdlqlsz8123assaa',
  LogicalName: 'locale',
  VATRegisteredLabel: 'AT401',
  VATRegisteredValue: 1000001
}

我写了一个函数,它必须转换我的对象并用下一个类型返回它

type TransformedData {
  Id: string,
  LogicalName: string,
  VATRegisteredLabel: string | null,
  VATRegisteredValue: number | null
}

我的功能:

const _isNull = (value: any) => {
  let res = value === null || undefined ? null : value;
  return res
};

function transformData<T extends {}, U extends {}>(obj: T, fatherName: keyof U | undefined) {
  let newObj;

  for (let key in obj) {
      let k = obj[key];
      if (typeof k === 'object' && !Array.isArray(k) && k !== null) {
          Object.assign(newObj, transformData<typeof k, T>(k, key))
      } else {
          Object.assign(newObj, { [fatherName ? fatherName + key : key]: _isNull(k) })
      }
  }

  return newObj;
}

但是我获得了一个空对象类型的新对象。 有没有办法重写返回一个 TransformedData 类型的新对象的函数?

【问题讨论】:

  • 您的let newObj; 行根本没有创建任何对象,因此这将导致运行时错误。我认为这只是一个错字,我建议您至少将其更改为let newObj = {},以免运行时行为成为问题

标签: typescript


【解决方案1】:

我将这个问题解释为:“你如何在 TypeScript 中采用对象类型,例如

type Data = {
  Id: string,
  LogicalName: string,
  VATRegistered: {
    Label: string | null,
    Value: number | null,
    SomethingElse: {
       Hello: number
    }
  }
}

并递归地将其展平为如下对象类型:

type TransformedData = {
  Id: string,
  LogicalName: string,
  VATRegisteredLabel: string | null,
  VATRegisteredValue: number | null,
  VATRegisteredSomethingElseHello: number
}

所以所有属性都是非对象类型,而新类型中的每个键都是结果属性的连接键路径?”


我只想说这是可能,但脆弱而且丑得可怕。 TypeScript 4.1 为您提供了 recursive conditional typestemplate literal typeskey remapping in mapped types,所有这些都是必需的。从概念上讲,对于Flatten 一个对象,如果它们是基元或数组,您想要获取对象的每个属性并按原样输出它们,否则Flatten 它们。 Flatten 属性是将属性键添加到扁平属性的键之前。

这或多或少是我采用的方法,但是您必须跳过很多障碍(例如,避免递归限制、联合到交叉点、交叉点到单个对象、避免 symbol 键在键连接等),甚至很难开始更详细地解释它,并且有很多边缘情况和警告(例如,我预计可选属性、索引签名或属性类型会发生不好的事情是具有至少一个对象类型成员的联合),我不愿意在生产环境中使用这样的东西。无论如何,这里是它所有的 ? 荣耀:

type Flatten<T extends object> = object extends T ? object : {
  [K in keyof T]-?: (x: NonNullable<T[K]> extends infer V ? V extends object ?
    V extends readonly any[] ? Pick<T, K> : Flatten<V> extends infer FV ? ({
      [P in keyof FV as `${Extract<K, string | number>}${Extract<P, string | number>}`]:
      FV[P] }) : never : Pick<T, K> : never
  ) => void } extends Record<keyof T, (y: infer O) => void> ?
  O extends infer U ? { [K in keyof O]: O[K] } : never : never

然后您的 transformData() 函数可以被赋予以下调用签名(我使用的是 overload 并且我只关心当您在没有 fatherName 参数的情况下调用它时的行为。剩下的我会给出如any:

function transformData<T extends object>(obj: T): Flatten<T>;
function transformData(obj: any, fatherName: string | number): any
function transformData(obj: any, fatherName?: string | number): any {
  let newObj = {};
  for (let key in obj) {
    let k = obj[key];
    if (typeof k === 'object' && !Array.isArray(k) && k !== null) {
      Object.assign(newObj, transformData(k, key))
    } else {
      Object.assign(newObj, { [fatherName ? fatherName + key : key]: _isNull(k) })
    }
  }
  return newObj;
}

让我们看看它在这个data 上是如何工作的:

const data: Data = {
  Id: 'qK1jd828Qkdlqlsz8123assaa',
  LogicalName: 'locale',
  VATRegistered: {
    Label: 'AT401',
    Value: 1000001,
    SomethingElse: {
      Hello: 123
    }
  }
}

const transformed = transformData(data);
/* const transformed: {
    Id: string;
    LogicalName: string;
    VATRegisteredLabel: string | null;
    VATRegisteredValue: number | null;
    VATRegisteredSomethingElseHello: number;
} */

console.log(transformed);
/*  {
  "Id": "qK1jd828Qkdlqlsz8123assaa",
  "LogicalName": "locale",
  "VATRegisteredLabel": "AT401",
  "VATRegisteredValue": 1000001,
  "SomethingElseHello": 123
} */

万岁,编译器发现transformedTransformedData 属于同一类型,即使我没有这样注释它。键在类型和对象中连接。

那么,就这样吧。同样,我真的只建议将其用于娱乐目的,以了解我们可以将类型系统推到多远。对于任何生产用途,我可能只是硬编码 (obj: Data) =&gt; TransformedData 类型的调用签名,如果那是您使用它的目的,或者甚至可能坚持使用 any 并告诉人们他们需要编写自己的类型他们叫它。

Playground link to code

【讨论】:

  • 非常感谢。我会试着弄清楚 Flatten 类型会发生什么
【解决方案2】:

这是一个更详细但更易读的实现:

// Returns R if T is a function, otherwise returns Fallback
type IsFunction<T, R, Fallback = T> = T extends (...args: any[]) => any ? R : Fallback

// Returns R if T is an object, otherwise returns Fallback
type IsObject<T, R, Fallback = T> = IsFunction<T, Fallback, (T extends object ? R : Fallback)>

// "a.b.c" => "b.c"
type Tail<S> = S extends `${string}.${infer T}`? Tail<T> : S;

// typeof Object.values(T)
type Value<T> = T[keyof T]

// {a: {b: 1, c: 2}} => {"a.b": {b: 1, c: 2}, "a.c": {b: 1, c: 2}}
type FlattenStepOne<T> = {
  [K in keyof T as K extends string ? (IsObject<T[K], `${K}.${keyof T[K] & string}`, K>) : K]: 
    IsObject<T[K], {[key in keyof T[K]]: T[K][key]}>
};

// {"a.b": {b: 1, c: 2}, "a.c": {b: 1, c: 2}} => {"a.b": {b: 1}, "a.c": {c: 2}}
type FlattenStepTwo<T> = {[a in keyof T]:  IsObject<T[a], Value<{[M in keyof T[a] as M extends Tail<a> ? M : never]: T[a][M] }>>}

// {a: {b: 1, c: {d: 1}}} => {"a.b": 1, "a.c": {d: 1}}
type FlattenOneLevel<T> = FlattenStepTwo<FlattenStepOne<T>>

// {a: {b: 1, c: {d: 1}}} => {"a.b": 1, "a.b.c.d": 1}
type Flatten<T> = T extends FlattenOneLevel<T> ? T: Flatten<FlattenOneLevel<T>>

它不支持所有边缘情况,例如可选属性,但它应该不会太难适应您遇到的特定边缘情况。

例如,删除代码中键名中的点:

// "a.b.c" => "abc"
type RemoveDots<S> = S extends `${infer H}.${infer T}`? RemoveDots<`${H}${T}`> : S;

type FlattenWithoutDots<T> = {[K in Flatten<T> as RemoveDots<K>]: Flatten<T>[K]};

【讨论】:

  • 这太完美了!对于大型对象,最佳答案提出“类型实例化过深并且可能无限”,但你的工作恰到好处!
【解决方案3】:

试试这个:

function parseObject<T>(data:T, prefix:string) {
  return Object.entries(data).reduce(
    (prev, [key, value]) => ({
      ...prev,
      [`${prefix}${key}`]: value ?? null
    }),
    {}
  );
}

function parseSingle({ Id, LogicalName, VATRegistered }: Data):TransformedData {
  return <TransformedData>({ Id, LogicalName, ...parseObject(VATRegistered,'VATRegistered') })
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-08-12
    • 2023-04-08
    • 1970-01-01
    • 2020-10-02
    • 1970-01-01
    • 1970-01-01
    • 2017-03-19
    • 1970-01-01
    相关资源
    最近更新 更多