Hone logo
Hone
Problems

Building a Deeply Nested Type Transformer in TypeScript

This challenge focuses on leveraging TypeScript's advanced type system, specifically recursive conditional types, to transform deeply nested data structures. You will learn how to create a type that can inspect and modify elements within arbitrarily nested arrays and objects. This skill is invaluable for tasks like deep data validation, schema transformation, or creating type-safe utilities for manipulating complex data.

Problem Description

Your task is to create a TypeScript utility type called DeepTransformer. This type should accept two generic arguments:

  1. T: The type of the data structure to be transformed. This can be a primitive, an object, or an array, potentially with deeply nested structures.
  2. Transformer: A conditional type that defines how to transform individual elements. This Transformer type will be applied recursively.

The DeepTransformer type should recursively traverse the structure of T.

  • If an element within T matches the conditions defined in Transformer, it should be transformed according to Transformer.
  • If an element is an array, DeepTransformer should recursively apply itself to each element of the array.
  • If an element is an object, DeepTransformer should recursively apply itself to each of its properties.
  • If an element is a primitive or does not match any conditions in Transformer, it should remain unchanged.

Key Requirements:

  • The DeepTransformer type must be recursive.
  • It must handle primitives, objects, and arrays.
  • It should correctly apply the Transformer type to individual values.

Expected Behavior:

The DeepTransformer should produce a new type that mirrors the structure of the input T, but with elements transformed according to the Transformer logic.

Edge Cases to Consider:

  • Empty arrays and objects.
  • null and undefined values.
  • Circular references (while TypeScript doesn't fully support runtime circularity, consider how your type might behave conceptually if you were to encounter them, although for this exercise, we'll assume no runtime circular references).

Examples

Example 1: Transforming strings to uppercase and numbers to their double.

// Transformer definition:
type StringToUppercase<T> = T extends string ? Uppercase<T> : T;
type DoubleNumber<T> = T extends number ? T | number extends infer N ? N extends number ? N * 2 : never : never : T;

// Combining transformers (for demonstration, not required in the problem itself)
// type MyComplexTransformer<T> = T extends string ? Uppercase<T> : T extends number ? T * 2 : T;

// Assume DeepTransformer is implemented as described above
type InputData1 = {
  name: string;
  age: number;
  address: {
    street: string;
    zip: number;
  };
  tags: (string | number)[];
};

// Expected DeepTransformer implementation will recursively apply StringToUppercase and DoubleNumber
// type TransformedData1 = DeepTransformer<InputData1, MyComplexTransformer<any>>;

// For this example, let's assume a simplified transformer for clarity:
type Transformer1<T> = T extends string ? Uppercase<T> : T extends number ? T * 2 : T;
type TransformedData1 = DeepTransformer<InputData1, Transformer1<any>>;

/*
Expected TransformedData1:
{
  name: string; // Uppercase<string> -> string (no change in type, but conceptually would be uppercase)
  age: number;  // number * 2
  address: {
    street: string; // Uppercase<string> -> string
    zip: number;    // number * 2
  };
  tags: (string | number)[]; // Each element transformed
}
*/

Example 2: Marking all primitive values as optional.

// Transformer definition:
type MakeOptional<T> = T extends string | number | boolean | null | undefined ? T | undefined : T;

type InputData2 = {
  id: number;
  isActive: boolean;
  details: {
    description: string | null;
    settings: {
      timeout: number;
    };
  };
  items: (string | number)[];
};

// Assume DeepTransformer is implemented
type TransformedData2 = DeepTransformer<InputData2, MakeOptional<any>>;

/*
Expected TransformedData2:
{
  id: number | undefined;
  isActive: boolean | undefined;
  details: {
    description: string | null | undefined;
    settings: {
      timeout: number | undefined;
    };
  };
  items: (string | undefined | number | undefined)[]; // or (string | number | undefined)[]
}
*/

Example 3: A deeply nested array transformation.

type InputArray3 = (number | string | (number | string)[])[];

// Transformer: Convert everything to string
type ToStringTransformer<T> = T extends string ? string : T extends number ? string : T;

// Assume DeepTransformer is implemented
type TransformedArray3 = DeepTransformer<InputArray3, ToStringTransformer<any>>;

/*
Expected TransformedArray3:
string | (string | string[])[]
// Or more precisely:
(string | (string | string[])[])[]

Breakdown:
- Top-level array elements:
  - number -> string
  - string -> string
  - (number | string)[] -> recursively apply ToStringTransformer to elements
    - number -> string
    - string -> string
*/

Constraints

  • The DeepTransformer type must be a single utility type.
  • The solution should focus purely on type-level programming in TypeScript. No runtime JavaScript code is required.
  • Assume that the input types T will not contain functions or complex, non-plain object types that are difficult to represent statically.

Notes

This problem requires you to think recursively. Consider how you would handle the base cases (primitives) and the recursive steps (objects and arrays). The Transformer generic will be a conditional type itself, allowing for flexible transformation logic. You might find it helpful to use mapped types and conditional type inference.

Good luck!

Loading editor...
typescript