Implement Pick and Omit Utility Types in TypeScript
TypeScript's built-in utility types are incredibly powerful for manipulating existing types. This challenge asks you to recreate two fundamental utility types: Pick and Omit. Understanding how these work will deepen your grasp of generic types, mapped types, and conditional types in TypeScript.
Problem Description
You need to implement two generic TypeScript utility types:
MyPick<T, K>: This utility type should construct a type by picking a set of propertiesKfrom typeT.MyOmit<T, K>: This utility type should construct a type by omitting a set of propertiesKfrom typeT.
Key Requirements:
- Both
MyPickandMyOmitshould be generic types that accept two type arguments:T: The source object type.K: A union of property keys to include (forMyPick) or exclude (forMyOmit).Kcan be a literal type or a union of literal types.
- The resulting type should accurately reflect the selected/omitted properties and their original types.
- The implementation should leverage TypeScript's advanced type manipulation features.
Expected Behavior:
MyPickshould create a new type containing only the properties specified inK.MyOmitshould create a new type containing all properties ofTexcept those specified inK.
Edge Cases to Consider:
- What happens if
Kcontains keys that don't exist inT? The standardPickandOmittypes ignore non-existent keys, and your implementation should behave similarly. - What if
Tis an empty object type? - What if
Kis an empty union type?
Examples
Example 1: MyPick
interface Todo {
title: string;
description: string;
completed: boolean;
}
type MyPick<T, K extends keyof T> = {
// Your implementation here
};
type TodoPreview = MyPick<Todo, "title" | "completed">;
// Expected type:
// {
// title: string;
// completed: boolean;
// }
const todoPreview: TodoPreview = {
title: "Learn TypeScript",
completed: false,
};
Explanation: MyPick<Todo, "title" | "completed"> selects only the title and completed properties from the Todo interface.
Example 2: MyOmit
interface Person {
name: string;
age: number;
address: string;
phone?: string;
}
type MyOmit<T, K extends keyof T> = {
// Your implementation here
};
type PersonWithoutAddress = MyOmit<Person, "address">;
// Expected type:
// {
// name: string;
// age: number;
// phone?: string;
// }
const personInfo: PersonWithoutAddress = {
name: "Alice",
age: 30,
phone: "123-456-7890",
};
Explanation: MyOmit<Person, "address"> creates a new type from Person but excludes the address property.
Example 3: MyOmit with non-existent key
interface User {
id: number;
username: string;
}
type UserWithoutSecret = MyOmit<User, "password">;
// Expected type:
// {
// id: number;
// username: string;
// }
Explanation: MyOmit<User, "password"> attempts to omit "password". Since "password" does not exist in User, it is effectively ignored, and the resulting type is identical to User.
Constraints
- Your solution must be pure TypeScript, using only type-level programming.
- The implementations of
MyPickandMyOmitshould reside in generic type aliases. - The
Kparameter should be constrained tokeyof TforMyPickandkeyof TforMyOmitin your initial thought process, but the final solution should handle cases whereKmight include keys not inTgracefully (as per standard behavior).
Notes
- Consider using Mapped Types and Conditional Types (specifically,
nevertype and distributive conditional types) to achieve the desired outcomes. - Think about how you can iterate over the keys of
Tand conditionally include or exclude them based onK. - The built-in
keyofoperator will be very useful here. - For
MyOmit, you might need to use a type that represents "all keys of T except those in K".