Home > Back-end >  Type for function that recursively removes unnecessary container objects
Type for function that recursively removes unnecessary container objects

Time:10-08

I have objects being returned by a server that might contain container attributes.

Imagine an object like:

{
  attr1: string,
  attr2: number,
  attr3: {
    uselessArg1: ...,
    uselessArg2: ...,
    uselessArg3: ...,
    content: [subVal1, subVal2, ...]
  }
}

I have a function that would grab that object and return it like this:

{
  attr1: string,
  attr2: number,
  attr3: [subVal1, subVal2, ...]
}

Now, the real problem is that any object might have an attribute that has the object type with the useless keys and the content sub-attribute, including the objects in the redundant array

So we could imagine that any given object inherits from this interface:

interface MaybeHasContent {
  [key:string]?: {
    uselessArg1: ...,
    uselessArg2: ...,
    uselessArg3: ...,
    content:this[]
  } 
}

My utility function first validates whether any key in the object has a content subkey, if found, hoists the value of the content subkey to its parent key, which clears all the useless attributes as well. Then it keeps calling itself on any child object of the current working object and also on any object inside the content array. It essentially runs through all the branches of an object, including branches that have object arrays, and hoists a very specific key one level up

How do I write an interface for this monster? I would like TS to understand that if I have an object where I can write myObj.val1.redundant[0].name, then after passing it through my utility function this would be a legal call: myObj.val1[0].name

CodePudding user response:

Here's one possible approach:

declare function clean<T>(t: T): Clean<T>;

type Clean<T> = T extends object ? (
    T extends { content: infer U } ?
    Clean<U> : { [K in keyof T]: Clean<T[K]> }
) : T;

Your function takes a value t of generic type T, and returns a value of type Clean<T>.

And Clean<T> is a recursive conditional type representing what clean() does. If T is of a non-primitive object type, then we check to see if has a content property. If so, we extract that property (using conditional type inference) and pass it as the new type argument to Clean. If not, then we map the object type T to a version where each property value has Clean applied to it. (Note that this also takes care of array types, since mapped types on arrays and tuples become arrays and tuples.) Finally, if T is a primitive type, we just return T.


Let's test it out:

const cleaned = clean({
    attr1: "abc",
    attr2: 123,
    attr3: {
        uselessArg1: 1,
        uselessArg2: 2,
        uselessArg3: 3,
        content: [
            { a: 1, b: 2, c: { content: "a", x: 1 } },
            { a: 3, b: 4, c: { content: "b", x: 2 } }
        ]
    }
});
/* const cleaned: {
    attr1: string;
    attr2: number;
    attr3: {
        a: number;
        b: number;
        c: string;
    }[];
} */

Looks good. The type of cleaned is the same as the input type with all the content properties "hoisted up". So attr3 doesn't have the uselessArg properties. It's just an array. And the elements of that array have a c property that's just a string, because the c property of the input array had a string-valued content property itself.


There are always edge cases with such recursive type utilities. For example, it would be a bad idea if you tried to clean a pathologically recursive data structure:

interface ContentOuroboros {
    content: ContentOuroboros
}
type OhNoes = Clean<ContentOuroboros> // error!
// Type instantiation is excessively deep and possibly infinite.

I imagine you're not likely to do that, but it's hard for me to think of all possible pitfalls in advance. So be sure to test it against your use cases and tweak if necessary.

Playground link to code

  • Related