Hone logo
Hone
Problems

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:

  1. MyPick<T, K>: This utility type should construct a type by picking a set of properties K from type T.
  2. MyOmit<T, K>: This utility type should construct a type by omitting a set of properties K from type T.

Key Requirements:

  • Both MyPick and MyOmit should be generic types that accept two type arguments:
    • T: The source object type.
    • K: A union of property keys to include (for MyPick) or exclude (for MyOmit). K can 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:

  • MyPick should create a new type containing only the properties specified in K.
  • MyOmit should create a new type containing all properties of T except those specified in K.

Edge Cases to Consider:

  • What happens if K contains keys that don't exist in T? The standard Pick and Omit types ignore non-existent keys, and your implementation should behave similarly.
  • What if T is an empty object type?
  • What if K is 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 MyPick and MyOmit should reside in generic type aliases.
  • The K parameter should be constrained to keyof T for MyPick and keyof T for MyOmit in your initial thought process, but the final solution should handle cases where K might include keys not in T gracefully (as per standard behavior).

Notes

  • Consider using Mapped Types and Conditional Types (specifically, never type and distributive conditional types) to achieve the desired outcomes.
  • Think about how you can iterate over the keys of T and conditionally include or exclude them based on K.
  • The built-in keyof operator will be very useful here.
  • For MyOmit, you might need to use a type that represents "all keys of T except those in K".
Loading editor...
typescript