There might be a better approach for dealing with this, however, this is the best that I came up with:
Utility types
Prettify
- is a utility to make types easy to read:
type Prettify<T> = T extends infer R
? {
[K in keyof R]: R[K];
}
: never;
UnionToIntersection
- turns union types to intersection. Reference:
type UnionToIntersection<U> = (
U extends any ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never;
Solution
Your implementation is almost finished except for the last &
part. In your implementation, you can't check for the ?
identifier. Therefore we will change the logic of recursion.
First, let's infer a type for the rest of the keys that are not in Property
:
Exclude<keyof Type, Property> extends infer Rest extends string.
Then, we have to distribute the union members in Rest
to treat the remaining keys separately; otherwise, If one of the properties in the Rest
is optional and the other is not, in the end, both of them will be marked as optional, which is not expected. We will distribute the Rest
with the following condition:
Rest extends Rest ? ... : never
If this condition is true the following code will be executed for each of the Rest
union's members separately.
To check if some property is optional or not, we can pick the piece that we are testing and check if it extends its' required version, which can be done as follows:
Pick<Type, Rest> extends Required<Pick<Type, Rest>> ? true : false
If this condition is true then the property is required, otherwise, it is optional:
Pick<Type, Rest> extends Required<Pick<Type, Rest>>
? { [Key in Rest]: DeepPartial<Type[Key], Property> }
: { [Key in Rest]?: DeepPartial<Type[Key], Property> }
: never
As we have distributed the Rest
this whole piece will result in the union; however, they should be just one object; thus, we will use UnionToIntersection
to convert it to a single object and for it to look pretty, we will wrap the whole DeepPartial
in Prettify
.
Full code:
type DeepPartial<Type, Property> = Prettify<
Type extends Array<infer ArrayType>
? Array<DeepPartial<ArrayType, Property>>
: Type extends Record<string, unknown>
? {
[Key in Extract<keyof Type, Property>]?: DeepPartial<
Type[Key],
Property
>;
} & UnionToIntersection<
Exclude<keyof Type, Property> extends infer Rest extends string
? Rest extends Rest
? Pick<Type, Rest> extends Required<Pick<Type, Rest>>
? { [Key in Rest]: DeepPartial<Type[Key], Property> }
: { [Key in Rest]?: DeepPartial<Type[Key], Property> }
: never
: never
>
: Type
>;
playground