Solution:
type Increment<A extends number[]> = [...A, 0];
type DeepPartial<
T,
Depth extends number = 0,
CurrentDepth extends number[] = []
> = T extends PrimitiveType
? T
: CurrentDepth["length"] extends Depth
? T
: {
[K in keyof T]?: DeepPartial<T[K], Depth, Increment<CurrentDepth>>;
};
type PartialWithLevelTwo = DeepPartial<p3, 2>;
const partialItem: PartialWithLevelTwo = {};
type PrimitiveType =
| string
| number
| boolean
| undefined
| null
| symbol
| bigint;
Explanation:
We first define a utility type Increment that increments the length of the provided tuple.
Then we define DeepPartial; it takes T, the type to operate on, Depth, the desired depth (by default it is 0), and CurrentDepth, the current depth we are at.
If T extends a primitive, we just return T.
However, if it isn't it must be an object of some sort.
If the current depth we are at exceeds the depth limit, we stop and return T.
If we can still go on, we do what Partial does.
The only thing different is that instead of T[K] we use DeepPartial again, provide the same depth, and increment the current depth.
A fair note: because this expects number[] as the depth (using the length of the array as our number here), you must provide [0, 0] if you want a depth of 2 etc.
You can however, write a type that can create an array of length N filled with 0's.
Playground
Here's a version that uses numbers instead of arrays but is limited by Increment's range