Hone logo
Hone
Problems

Deeply Immutable Types in TypeScript

TypeScript's readonly modifier provides a way to make properties of an object or elements of an array immutable. However, this immutability is shallow; nested objects and arrays remain mutable. This challenge asks you to create a utility type that recursively applies readonly to all properties and elements of a given type, ensuring complete deep immutability. This is crucial for building predictable and maintainable applications, especially in state management scenarios where accidental mutations can lead to hard-to-debug issues.

Problem Description

You need to implement a TypeScript utility type called DeepReadonly<T>. This type should take a generic type T as input and return a new type where all properties and array elements are recursively made readonly.

Key Requirements:

  • Recursive Application: The readonly modifier should be applied not just to the top-level properties of an object or elements of an array, but also to all nested objects and arrays within T.
  • Object Properties: For object types, each of their properties should become readonly.
  • Array Elements: For array types, their elements should become readonly.
  • Primitive Types: Primitive types (string, number, boolean, null, undefined, symbol, bigint) should remain unchanged, as they are already immutable.
  • Function Types: Function types should also remain unchanged.
  • Handling null and undefined: These should be handled gracefully.

Expected Behavior:

When DeepReadonly<T> is applied to a type, the resulting type should prevent any assignment to its properties or elements, even if they are nested deep within the structure.

Edge Cases to Consider:

  • Empty objects and arrays.
  • Types that are already partially readonly.
  • Complex nested structures with mixed object and array types.
  • Union types.
  • Intersection types.

Examples

Example 1:

Input: {
  a: number;
  b: {
    c: string;
    d: {
      e: boolean[];
    };
  };
  f: (x: number) => number;
}

Output:
{
  readonly a: number;
  readonly b: {
    readonly c: string;
    readonly d: {
      readonly e: readonly (boolean)[];
    };
  };
  readonly f: (x: number) => number;
}

Explanation:
The `DeepReadonly` type recursively applies `readonly` to all properties. 'a', 'b', and 'f' become readonly. 'b' is an object, so its properties 'c' and 'd' also become readonly. 'd' is an object with property 'e', which is an array of booleans. 'e' and its elements (boolean) become readonly. The function type of 'f' remains unchanged.

Example 2:

Input: number[][] | { baz: string }

Output:
readonly (readonly number[])[] | { readonly baz: string }

Explanation:
When the input is a union type, `DeepReadonly` should be applied to each part of the union. The `number[][]` becomes `readonly (readonly number[])[]`, and `{ baz: string }` becomes `{ readonly baz: string }`.

Example 3:

Input: [1, { a: 2 }]

Output:
readonly [1, { readonly a: 2 }]

Explanation:
This example demonstrates a tuple type. `DeepReadonly` should correctly handle tuple elements, making nested object properties readonly.

Constraints

  • The solution must be implemented entirely using TypeScript's type system.
  • The solution should be efficient and not lead to excessive compile times for moderately complex types.
  • The solution should not rely on any external libraries or packages.

Notes

Consider how to differentiate between object types and array types to apply readonly appropriately. You'll likely need conditional types and mapped types. Remember that readonly applied to an array makes the array itself readonly (you can't push/pop/splice), but its elements can still be mutated if they are objects or arrays. Your DeepReadonly must address this by making the elements themselves readonly as well. You might need to check if a type is an array before recursively applying DeepReadonly.

Loading editor...
typescript