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:
- Removes all entries of kind
TKey: PrimitiveType
in T - 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