Advanced Mapped Types: Building a Recursive DeepPartial Utility
Mapped types in TypeScript are a powerful tool for transforming existing types. This challenge will push your understanding by requiring you to build a recursive DeepPartial utility type, enabling you to make all properties of a nested object optional. This is incredibly useful for scenarios like creating default configurations, handling optional user input, or working with API responses where fields might be missing.
Problem Description
Your task is to create a TypeScript mapped type called DeepPartial<T>. This type should take an input type T and return a new type where every property, and every property within nested objects and arrays, is made optional (i.e., their type is updated to T[K] | undefined).
Key Requirements:
- Shallow Partial: For non-object types, the property should become
undefined. - Recursive Deep Partial: For object types, recursively apply
DeepPartialto each of its properties. - Array Handling: If a property is an array of objects, each element within the array should also be deeply partialized.
- Primitive Type Preservation: Primitive types (like
string,number,boolean,null,undefined,symbol,bigint) should simply becomeType | undefined.
Expected Behavior:
The DeepPartial<T> type should correctly transform complex, nested object structures, ensuring all levels of nesting are made optional.
Edge Cases to Consider:
- Empty objects (
{}). - Arrays of primitives.
- Arrays of objects.
- Readonly properties.
- Functions (should probably remain functions).
Examples
Example 1:
interface User {
id: number;
name: string;
address: {
street: string;
city: string;
zip: number;
};
roles: string[];
preferences: {
theme: 'dark' | 'light';
notifications: {
email: boolean;
sms: boolean;
};
};
}
type PartialUser = DeepPartial<User>;
/* Expected PartialUser:
{
id?: number | undefined;
name?: string | undefined;
address?: {
street?: string | undefined;
city?: string | undefined;
zip?: number | undefined;
} | undefined;
roles?: (string | undefined)[] | undefined; // Arrays of primitives are tricky, focus on arrays of objects for the deep part. For now, assume primitives in arrays can be undefined.
preferences?: {
theme?: ('dark' | 'light') | undefined;
notifications?: {
email?: boolean | undefined;
sms?: boolean | undefined;
} | undefined;
} | undefined;
}
*/
Example 2:
type Person = {
name: string;
age: number;
contact: {
email: string;
phone?: string;
};
tags: Array<{ id: number; value: string }>;
};
type PartialPerson = DeepPartial<Person>;
/* Expected PartialPerson:
{
name?: string | undefined;
age?: number | undefined;
contact?: {
email?: string | undefined;
phone?: string | undefined; // Existing optional property remains optional
} | undefined;
tags?: Array<{ id?: number | undefined; value?: string | undefined }> | undefined;
}
*/
Example 3: Empty Object and Primitives
type EmptyObj = {};
type PartialEmptyObj = DeepPartial<EmptyObj>; // Should be {}
type Primitive = number;
type PartialPrimitive = DeepPartial<Primitive>; // Should be number | undefined
Constraints
- The solution must be a single TypeScript mapped type named
DeepPartial<T>. - You should not use any external libraries or packages.
- Your solution should handle nested objects and arrays of objects. For arrays of primitives, the elements themselves can be made optional (e.g.,
string | undefined).
Notes
- Consider how to distinguish between object types and other types. The
keyof Toperator and conditional types will be your friends here. - For arrays, you'll need to think about how to access the type of the elements within the array.
Readonly<T>might be useful for handling readonly properties, but the primary goal is to make properties optional, so a simple| undefinedshould suffice.- Functions are generally not meant to be made "partial" in the same way as data objects. You can choose to preserve their type or make them
Function | undefinedif you feel it's appropriate for your interpretation. For this challenge, preserving the function type is a reasonable approach if encountered.