Advanced Mapped Types: Utility Types in TypeScript
Mapped types are a powerful feature in TypeScript that allow you to transform existing types into new ones. This challenge focuses on building several common utility types using mapped types, demonstrating your understanding of conditional types, key remapping, and type transformations. Creating these utility types is crucial for writing reusable and maintainable TypeScript code.
Problem Description
Your task is to implement several utility types using TypeScript's mapped types. These utility types are commonly used in libraries and frameworks to provide flexible and type-safe operations on objects. You will need to define these types using mapped types, conditional types, and potentially other advanced TypeScript features.
Specifically, you need to implement the following utility types:
PartialBy<T, K>: Makes a subset of properties inToptional.Kis a type representing the keys to make optional.ReadonlyBy<T, K>: Makes a subset of properties inTreadonly.Kis a type representing the keys to make readonly.PickBy<T, K>: Creates a new type by picking a subset of properties fromT.Kis a type representing the keys to pick.OmitBy<T, K>: Creates a new type by omitting a subset of properties fromT.Kis a type representing the keys to omit.DeepPartial<T>: Recursively makes all properties ofToptional, including properties of nested objects.
Examples
Example 1: PartialBy
Input:
interface Person {
name: string;
age: number;
address: {
street: string;
city: string;
};
}
type PartialPersonAddress = PartialBy<Person, 'address'>;
Output:
type PartialPersonAddress = {
name: string;
age: number;
address?: {
street: string;
city: string;
};
}
Explanation: The 'address' property (and its nested properties) are now optional.
Example 2: ReadonlyBy
Input:
interface Product {
id: number;
name: string;
price: number;
description?: string;
}
type ReadonlyProductId = ReadonlyBy<Product, 'id'>;
Output:
type ReadonlyProductId = {
readonly id: number;
name: string;
price: number;
description?: string;
}
Explanation: The 'id' property is now readonly.
Example 3: PickBy
Input:
interface Config {
apiUrl: string;
timeout: number;
debug: boolean;
}
type PickedConfig = PickBy<Config, 'apiUrl' | 'debug'>;
Output:
type PickedConfig = {
apiUrl: string;
debug: boolean;
}
Explanation: Only 'apiUrl' and 'debug' properties are included in the new type.
Example 4: OmitBy
Input:
interface User {
id: number;
username: string;
email: string;
}
type UserWithoutId = OmitBy<User, 'id'>;
Output:
type UserWithoutId = {
username: string;
email: string;
}
Explanation: The 'id' property is omitted from the new type.
Example 5: DeepPartial
Input:
interface Settings {
theme: 'light' | 'dark';
notifications: {
email: boolean;
push: boolean;
};
}
type DeeplyPartialSettings = DeepPartial<Settings>;
Output:
type DeeplyPartialSettings = {
theme?: 'light' | 'dark';
notifications?: {
email?: boolean;
push?: boolean;
};
}
Explanation: All properties, including nested ones, are made optional.
Constraints
- All utility types must be implemented using mapped types and conditional types.
- The code should be well-formatted and easy to understand.
- The types should be generic and work with any valid TypeScript type.
- The
DeepPartialtype must handle arbitrarily nested objects. - No external libraries are allowed.
Notes
- Consider using
keyofto iterate over the keys of a type. - Conditional types (
T extends U ? X : Y) are essential for implementing these utility types. - Think about how to handle nested objects when creating
DeepPartial. Recursion might be helpful. - Pay close attention to the type signatures and ensure they accurately reflect the intended behavior.
- Start with
PartialByandReadonlyByas they are generally simpler, then move on toPickByandOmitBy, and finally tackleDeepPartial.