Hone logo
Hone
Problems

Advanced TypeScript: Mastering Intersection Types

TypeScript's intersection types (&) are a powerful tool for combining multiple types into one. They allow you to create complex types by merging the properties and methods of several simpler types. This challenge will test your understanding and ability to build sophisticated utility types that leverage intersection types to solve common programming scenarios.

Problem Description

Your task is to create a set of TypeScript utility types that enable more dynamic and flexible manipulation of types using intersections. Specifically, you need to implement the following:

  1. UnionToIntersection<U>: A utility type that converts a union type into an intersection type. This is useful when you want to ensure that an object possesses all properties from a union of types, rather than just one.

  2. IntersectionToUnion<T>: A utility type that converts an intersection type back into a union type. This is the inverse of UnionToIntersection and can be helpful in scenarios where you need to extract the individual types that formed an intersection.

  3. Merge<T, U>: A utility type that merges two types T and U. If a property exists in both T and U, the property in U should take precedence. This is akin to a shallow merge operation at the type level.

  4. OmitDeep<T, K>: A utility type that recursively omits properties specified by K from T. This means if a property in T is an object, and K includes a key within that nested object, that key should also be omitted from the nested object. K can be a single key or a union of keys.

Examples

Example 1: UnionToIntersection<U>

type SomeUnion = { a: string } | { b: number };
type IntersectionResult = UnionToIntersection<SomeUnion>;
// Expected type: { a: string } & { b: number }

Explanation: The SomeUnion type represents an object that can either have an a property or a b property. UnionToIntersection transforms this into a type that requires both an a property and a b property.

Example 2: IntersectionToUnion<T>

type SomeIntersection = { a: string } & { b: number };
type UnionResult = IntersectionToUnion<SomeIntersection>;
// Expected type: { a: string } | { b: number }

Explanation: The SomeIntersection type represents an object that has both an a and a b property. IntersectionToUnion transforms this back into a union type, effectively reversing the operation from Example 1.

Example 3: Merge<T, U>

type TypeA = { id: number; name: string; config?: boolean };
type TypeB = { name: string; age: number };
type MergedType = Merge<TypeA, TypeB>;
// Expected type: { id: number; name: string; age: number; config?: boolean }

Explanation: TypeA and TypeB are merged. The name property from TypeB (which is identical to TypeA's) takes precedence (though in this case, they are the same). The age property from TypeB is added, and the config property from TypeA is preserved.

Example 4: OmitDeep<T, K>

type User = {
  id: number;
  name: string;
  address: {
    street: string;
    city: string;
    zip: string;
  };
  contact: {
    email: string;
    phone: string;
  };
};

type OmittedUser = OmitDeep<User, 'address' | 'contact.phone'>;
// Expected type:
// {
//   id: number;
//   name: string;
//   address: {
//     street: string;
//     city: string;
//   };
//   contact: {
//     email: string;
//   };
// }

Explanation: The OmitDeep utility recursively removes properties. It omits the entire address object and specifically the phone property from within the contact object.

Constraints

  • Your solutions should be pure TypeScript utility types. No runtime JavaScript is expected.
  • The utility types should be generic and work with any valid TypeScript types.
  • UnionToIntersection and IntersectionToUnion should be robust enough to handle unions/intersections of primitive types as well as object types.
  • Merge should handle properties present in only one of the types.
  • OmitDeep should handle nested objects and a union of keys to omit. The key K for OmitDeep can be a simple key like 'address' or a dot-notation path like 'contact.phone'.

Notes

  • For UnionToIntersection, a common pattern involves distributing the union and then leveraging the behavior of function parameter types.
  • For IntersectionToUnion, think about how to "unwrap" the intersection.
  • For Merge, consider how to iterate over the keys of both types and combine them.
  • For OmitDeep, recursion and conditional types will be your friends. You'll likely need to parse the dot-notation path K to handle nested omissions. Consider helper types for parsing the path.
  • Success looks like having all four utility types correctly implemented and passing the provided examples.
Loading editor...
typescript