Hone logo
Hone
Problems

Advanced TypeScript Proxy Type Utilities

This challenge focuses on building robust and type-safe utility types in TypeScript for creating proxy objects. Proxy objects are powerful for intercepting and customizing operations on an object, and this challenge will equip you with the tools to define and work with them effectively at the type level. You'll create a type that represents a proxied version of another type, ensuring that the proxy's properties and methods are correctly inferred and constrained.

Problem Description

Your task is to create a TypeScript utility type, let's call it ProxyType<T, P>, which generates a type representing a proxy for a given base type T. The proxy type should also incorporate properties and behaviors defined by another type P, which represents the proxy's specific enhancements or overrides.

Key Requirements:

  1. Base Type Preservation: The ProxyType<T, P> should retain all properties and methods from the base type T.
  2. Proxy Overrides/Additions: The ProxyType<T, P> should allow for properties and methods defined in P to override or augment those from T. Specifically, if a property exists in both T and P, the type from P should take precedence. If a property only exists in P, it should be included.
  3. Readonly Support: The utility type should correctly handle readonly modifiers on properties from both T and P. If a property is readonly in T, it should remain readonly unless explicitly overridden as mutable in P. If a property is readonly in P, it should be readonly.
  4. Method Overriding: Methods in P should be able to override methods in T. The signature of the overridden method must be compatible with the original, but the implementation details are beyond the scope of this type utility.
  5. Index Signatures: The utility should correctly handle index signatures from both T and P.

Expected Behavior:

When defining a type ProxyType<T, P>, the resulting type should effectively be a merged type where P's members have higher precedence. This means:

  • If key is in T and P, the type of key in the result is the type from P.
  • If key is only in T, the type of key in the result is the type from T.
  • If key is only in P, the type of key in the result is the type from P.
  • readonly modifiers should be respected and propagated.

Edge Cases:

  • Consider cases where P is an empty type {}. The result should be identical to T.
  • Consider cases where T is an empty type {}. The result should be identical to P.
  • Handling of optional properties from both T and P.

Examples

Example 1:

interface Original {
  name: string;
  age: number;
  readonly id: string;
}

interface ProxyEnhancements {
  age: string; // Override 'age' with a different type
  address: string; // Add a new property
  readonly id: string; // Keep 'id' readonly
  getParent(): Original; // Add a new method
}

// Expected type:
// {
//   name: string;
//   age: string;
//   readonly id: string;
//   address: string;
//   getParent(): Original;
// }

Example 2:

interface Data {
  value: number;
  timestamp: Date;
}

interface DataProxy {
  value: number | null; // Make 'value' nullable
  createdAt: Date; // Add a new property
}

// Expected type:
// {
//   value: number | null;
//   timestamp: Date;
//   createdAt: Date;
// }

Example 3 (Readonly Propagation):

interface Config {
  timeout: number;
  readonly retries: number;
}

interface ConfigProxy {
  timeout: string; // Override 'timeout'
  retries: number; // Attempt to make 'retries' mutable (should remain readonly)
  logLevel: 'info' | 'warn' | 'error'; // Add new property
}

// Expected type:
// {
//   timeout: string;
//   readonly retries: number;
//   logLevel: 'info' | 'warn' | 'error';
// }

Example 4 (Index Signatures):

interface UserData {
  [key: string]: string | number;
  name: string;
}

interface UserDataProxy {
  [key: string]: string | boolean; // Override index signature
  email: string; // Add new property
  age: number; // Should be string | boolean due to index signature override
}

// Expected type:
// {
//   [key: string]: string | boolean;
//   name: string; // This specific property still needs to be string if not overridden
//   email: string;
//   age: number;
// }

Constraints

  • Your solution must be a single TypeScript utility type.
  • The utility type should be named ProxyType<T, P>.
  • The solution should not use any external libraries.
  • Focus on structural compatibility and type inference; the actual runtime behavior of a proxy object is not part of this challenge.

Notes

This challenge requires a deep understanding of TypeScript's mapped types, conditional types, and advanced type manipulation. Pay close attention to how readonly modifiers and index signatures interact. You'll likely need to combine several type manipulation techniques to achieve the desired outcome. Consider how to iterate over keys of both T and P and merge them according to the precedence rules.

Loading editor...
typescript