0

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: { };
}
Boom PT
  • 374
  • 4
  • 11

2 Answers2

2

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

so_close
  • 318
  • 1
  • 8
1

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

so_close
  • 318
  • 1
  • 8
  • Thanks for the insight. I have added some cases – Boom PT Jan 07 '23 at 17:04
  • Thanks! If my post was helpful, consider upvoting it. I'm just starting to earn reputation, so that would help me a lot! – so_close Jan 07 '23 at 17:06
  • 1
    @BoomPT please consider formatting your question in a form of e.g. unit tests: Maybe something like below: "I need to write a custom generic type Remove which behaves like below: // some optional comment with description type A1 = {...}; type B1 = {...}; Remove === B1; // some optional comment with description type A2 = {...}; type B2 = {...}; Remove === B2; This would make it a LOT easier to understand :) – so_close Jan 07 '23 at 17:08
  • 1
    @BoomPT from your examples it looks like what you want is the following: Write a generic type Remove 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. Did I understand you right? If so, please confirm, and I can write an answer for that. ALSO, please consider editing your question to include that formal description :) – so_close Jan 07 '23 at 17:14
  • The Omit type is good but I'm finding a way that doesn't need to explicitly writing all the path – Boom PT Jan 07 '23 at 17:16
  • @BoomPT I'm still having troubles understanding what you really need. If you could formalize it in a way of verbal description, or include more examples, I will help in writing a type :) – so_close Jan 07 '23 at 17:18
  • I want exact what you commented just that I don't want to require to write the path such as ... & { B: Omit & { E: Before["B"]["D"]["E"]; }; }; – Boom PT Jan 07 '23 at 17:24
  • Something like [this](https://stackoverflow.com/questions/68518274/how-to-deep-flatten-an-interface-in-typescript/68518494) stack overflow answer. It is almost perfect but it doesn't work if I want to remove only the nested value. Look at the comment I commented – Boom PT Jan 07 '23 at 17:28