Home > Back-end >  Typescript type utils flat map* (removing a level but keep its sublevel) while preserving object str
Typescript type utils flat map* (removing a level but keep its sublevel) while preserving object str

Time:01-08

Write a generic type Remove<T, key> which (a) removes all occurences of "key: PrimitiveType" e.g. "key: number;" in T, and (b) if T has "key: U" at some level, where U is not a primitive type, then it is converted into "U", and "key" is removed.

For example if I have the following type:

type Before = {
  A: string
  B: {
    C: string
    D: {
      E: number
    }
  }
}

and I want to change it to this by, for example, Remove<Before, "D">:

type After = {
  A: string
  B: {
    C: string
    E: number
  }
}

Note that D is removed but E is remained

Other cases worth mentioning thanks to so_close

Case #1 Remove<T,”data”>

type T = {
 data: {
   data: string;
 }
};

// would be
type T = { };

Case #2 Remove<T,”b”>

type T2 = {
  a: {
    b: string;
  };
  b: number;
}

// would be
type T2 = {
  a: { };
}

CodePudding user response:

I'll add another answer for clarity;

The problem we're trying to solve: Create a type Remove<T, TKey> which:

  1. Removes all entries of kind TKey: PrimitiveType in T
  2. Flattens all entries of kind TKey: ComplexType in T recursively

The following should work:

type Primitive = number | string | boolean;

// to satisfy Remove<{a : string;}, "a"> === {}
type ConvertPrimitiveToEmptyType<T> = T extends Primitive ? {} : T;

// if T contains a key with name <KeyName> at level 1, proceed recursively
type Unwrap<T, KeyName extends string> = KeyName extends keyof T
  ? Remove<ConvertPrimitiveToEmptyType<T[KeyName]>, KeyName>
  : {};

// separately process two parts of T:
// * part of T with all the keys except <KeyName>
// * part T[KeyName] if it exists 
type Remove<T, KeyName extends string> = {
    [key in keyof Omit<T, KeyName>]:
     T[key] extends Primitive 
      ? T[key]
      : Remove<T[key], KeyName> // part of T without KeyName
    } & Unwrap<T, KeyName>;

Let's test this using one of your examples!

type Before1 = {
  A: string
  B: {
    C: string
    D: {
      E: number
    }
  }
}

type ExpectedAfter1 = {
  A: string
  B: {
    C: string
    E: number
  }
}

type After1 = Remove<Before1, "D">;

We can test if types are equal using conditional types

// if A extends B, and B extends A, then B is equal to A
type CheckForEquality<A,B> = A extends B? B extends A ? true : false : false;

// if this type is "true" it means After1 is equal to ExpectedAfter1
type IsAfter1Good = CheckForEquality<After1, ExpectedAfter1>;

You can find more tests and live code at this TS playground

CodePudding user response:

I guess that your original question is:

Is it possible to write a generic type Remove<T, U>, which behaves as in the example

However, one example is not enough to answer this question. Consider the following:

type T = {
 data: {
   data: string;
 }
};

With T defined like above, how should your desired Remove<T, "data"> behave? Should it remove the deepest occurrence of the "data" field, resulting in { data: {}; }? Or should it remove the top-most, resulting in {}?

This goes on: what if we have the following type?

type T2 = {
  a: {
    b: string;
  };
  b: number;
}

How should Remove<T2, "b"> behave? Should it result in {a: {}; b: number} or in {a: { b: string; }; }

I cannot comment (I have low reputation), but please resolve mentioned ambiguities in your question. Without that, I'm afraid it's just not enough data to answer.

Try: adding more examples, or specifying verbally how your desired type should behave. Maybe, if you specify if verbally, it would turn out you already have the implementation written down in words!

P.S. If what you really wanted is just to convert Before to After using Typescript built-in utility types, you could do it like this:

type After = Omit<Before, "B"> & {
  B: Omit<Before["B"], "D"> & {
    E: Before["B"]["D"]["E"];
  };
};

This construction uses Omit to "forget" about what was defined in a particular field, but immediately after using Omit we specify the needed field, and we do it until we reach the needed level.

Unfortunately, it's not very elegant but the truth is: it's pretty much what Typescript has to offer

  • Related