Hone logo
Hone
Problems

TypeScript Structural Typing Utilities

TypeScript's structural typing system is powerful for ensuring type safety. However, sometimes we need to create types that are similar but not identical to existing ones, or to enforce specific structures at a deeper level. This challenge focuses on building utility types that leverage and extend TypeScript's structural typing capabilities to create more flexible and robust type definitions.

Problem Description

Your task is to implement several TypeScript utility types that help in manipulating and defining types based on their structure. These utilities will be crucial for scenarios where you need to:

  • Deeply merge object types: Combine properties from multiple object types, prioritizing properties from later types in the merge. This merge should be deep, meaning nested objects are also merged.
  • Extract properties based on a condition: Create a new type containing only the properties of a given object type that satisfy a certain condition (e.g., properties whose values are of a specific type).
  • Remove properties based on a condition: Create a new type containing all properties of a given object type except those that satisfy a certain condition.
  • Make specific properties optional: Create a new type where a designated subset of properties from an original type are made optional.

Key Requirements

You will need to implement the following utility types:

  1. DeepMerge<T, U>:

    • Merges two object types T and U.
    • If a property exists in both T and U:
      • If both values are object types, recursively merge them.
      • Otherwise, the property from U overrides the property from T.
    • If a property exists only in T, it's included.
    • If a property exists only in U, it's included.
    • This should work for any level of nesting.
    • Non-object properties in U should always override non-object properties in T.
  2. PickByType<T, V>:

    • Creates a new type containing only the properties from T whose value types are assignable to V.
  3. OmitByType<T, V>:

    • Creates a new type containing only the properties from T whose value types are not assignable to V.
  4. MakeOptional<T, K extends keyof T>:

    • Takes an object type T and a union of keys K.
    • Returns a new type where all properties specified in K are made optional in T. Properties not in K remain as they were in T.

Expected Behavior

The utility types should behave as described above, producing accurate and type-safe results when used in various TypeScript code scenarios.

Examples

Example 1: DeepMerge

type ObjA = {
  a: number;
  b: { c: string; d: boolean };
  e: { f: number };
};

type ObjB = {
  b: { c: string; g: null };
  e: { f: string };
  h: string;
};

type Merged = DeepMerge<ObjA, ObjB>;
// Expected type:
// {
//   a: number;
//   b: { c: string; d: boolean; g: null };
//   e: { f: string };
//   h: string;
// }

Example 2: PickByType and OmitByType

type Data = {
  id: number;
  name: string;
  isActive: boolean;
  createdAt: Date;
  tags: string[];
};

type StringProperties = PickByType<Data, string>;
// Expected type: { name: string }

type NonStringProperties = OmitByType<Data, string>;
// Expected type: { id: number; isActive: boolean; createdAt: Date; tags: string[] }

type PrimitiveProperties = PickByType<Data, string | number | boolean>;
// Expected type: { id: number; name: string; isActive: boolean }

Example 3: MakeOptional

interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

type UserWithOptionalEmail = MakeOptional<User, 'email'>;
// Expected type:
// {
//   id: number;
//   name: string;
//   email?: string;
//   role: 'admin' | 'user';
// }

type UserWithOptionalEmailAndRole = MakeOptional<User, 'email' | 'role'>;
// Expected type:
// {
//   id: number;
//   name: string;
//   email?: string;
//   role?: 'admin' | 'user';
// }

Constraints

  • The solution must be written entirely in TypeScript.
  • The utility types should be generic and work with any valid TypeScript types.
  • DeepMerge must handle nested objects recursively.
  • PickByType and OmitByType should correctly identify properties based on their value types.
  • MakeOptional should correctly target and make specific keys optional.
  • Avoid using external libraries or packages.
  • The provided examples should pass type checking with your implemented utilities.

Notes

  • Consider how TypeScript handles object types and conditional types for these implementations.
  • The DeepMerge utility is particularly challenging due to its recursive nature. You might need to define helper types to manage recursion.
  • For PickByType and OmitByType, remember that you can iterate over keys of a type and check their associated value types.
  • MakeOptional can be implemented by mapping over keys and conditionally making them optional.
  • Think about the base cases for recursion in DeepMerge.
  • The DeepMerge behavior for non-object properties is crucial: properties from the second type (U) always take precedence when they are not both objects that can be merged.
Loading editor...
typescript