如何展平具有嵌套子属性的对象类型?

WVF*_*IFE 9 typescript

我有数据类型的下一个数据

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
  }
}
Run Code Online (Sandbox Code Playgroud)

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

const transformedData = {
  Id: 'qK1jd828Qkdlqlsz8123assaa',
  LogicalName: 'locale',
  VATRegisteredLabel: 'AT401',
  VATRegisteredValue: 1000001
}
Run Code Online (Sandbox Code Playgroud)

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

type TransformedData {
  Id: string,
  LogicalName: string,
  VATRegisteredLabel: string | null,
  VATRegisteredValue: number | null
}
Run Code Online (Sandbox Code Playgroud)

我的功能:

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;
}
Run Code Online (Sandbox Code Playgroud)

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

coy*_*508 17

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

// 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>>
Run Code Online (Sandbox Code Playgroud)

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

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

// "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]};
Run Code Online (Sandbox Code Playgroud)

  • 太棒了!最重要的答案提出了“对于大型对象来说,类型实例化太深并且可能是无限的”,但你的效果恰到好处! (3认同)

jca*_*alz 11

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

type Data = {
  Id: string,
  LogicalName: string,
  VATRegistered: {
    Label: string | null,
    Value: number | null,
    SomethingElse: {
       Hello: number
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

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

type TransformedData = {
  Id: string,
  LogicalName: string,
  VATRegisteredLabel: string | null,
  VATRegisteredValue: number | null,
  VATRegisteredSomethingElseHello: number
}
Run Code Online (Sandbox Code Playgroud)

这样所有属性都是非对象类型,并且新类型中的每个键都是结果属性的串联键路径?”


我只想说这是可能的,但很脆弱而且丑陋得可怕。TypeScript 4.1 为您提供了递归条件类型模板文字类型以及映射类型中的键重新映射,所有这些都是您所需要的。从概念上讲,对于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
Run Code Online (Sandbox Code Playgroud)

然后你的transformData()函数可以得到以下调用签名(我正在使用重载并且只关心当你不带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;
}
Run Code Online (Sandbox Code Playgroud)

让我们看看它是如何工作的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
} */
Run Code Online (Sandbox Code Playgroud)

万岁,编译器认为它transformed是相同的类型,TransformedData即使我没有这样注释它。键在类型和对象中串联起来。

那么,就这样吧。再说一次,我真的只建议将其用于娱乐目的,以此来了解我们可以将类型系统推向多远。对于任何生产用途,我可能只是硬编码该类型的调用签名(obj: Data) => TransformedData(如果您正在使用它),或者甚至可能坚持下去any,只是告诉人们在调用它时需要编写自己的类型。

Playground 代码链接