Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
678 views
in Technique[技术] by (71.8m points)

typescript - How can an object type with nested subproperties be flattened?

I've the next data with the type 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
  }
}

And I have to convert it to the next one:

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

I've written a function which have to transform my object and return it with the next type

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

My function:

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;
}

But I gain a new object with the empty object type. Is there a way to rewrite the function that it returns a new object with TransformedData type?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

I am interpreting this question as: "How can you take an object type in TypeScript like

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

and recursively flatten it to an object type like:

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

so that all properties are non-object types, and where each key in the new type is the concatenated key path to the resulting property?"


Let me just say that this is possible but brittle and horrifically ugly. TypeScript 4.1 gives you recursive conditional types, template literal types, and key remapping in mapped types, all of which are needed. Conceptually, to Flatten an object you want to take each property of the object and output them as-is if they are primitives or arrays, and Flatten them otherwise. To Flatten a property is to prepend the properties key to the keys of the flattened properties.

This is more or less the approach I take, but there are so many hoops you have to jump through (e.g., avoiding recursion limits, unions-to-intersections, intersections-to-single-objects, avoiding symbol keys in key concatenation, etc) that it's hard to even begin to explain it in more detail, and there are so many edge cases and caveats (e.g., I'd expect bad things to happen with optional properties, index signatures, or property types which are unions with at least one object type member) that I'd be loath to use such a thing in production environments. Anyway, here it is in all its ?? glory:

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

Then your transformData() function could be given the following call signature (I'm using an overload and am only concerned about the behavior when you call it with no fatherName parameter. The rest I'll just give as 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;
}

Let's see how it works on this 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
} */

Hooray, the compiler sees that transformed is of the same type as TransformedData even though I didn't annotate it as such. The keys are concatenated in the type as well as the object.

So, there you go. Again, I really only recommend using this for entertainment purposes, as a way of seeing how far we can push the type system. For any production use I'd probably just hardcode a call signature of the type (obj: Data) => TransformedData if that's what you're using it for, or maybe even stick with any and just tell people they will need to write their own types when they call it.

Playground link to code


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...