Hone logo
Hone
Problems

Deep Readonly Types in TypeScript

Creating immutable data structures is crucial for building robust and predictable applications, especially in complex JavaScript environments like React or Redux. TypeScript's readonly modifier provides a good start, but it only applies to the top-level properties of an object or the elements of an array. This challenge asks you to create a utility type that recursively applies readonly to all properties of an object and all elements of nested arrays, ensuring complete immutability.

Problem Description

Your task is to define a TypeScript utility type named DeepReadonly<T>. This type should take a generic type T and return a new type where all properties of T are made readonly. Furthermore, if any of these properties are themselves objects or arrays, their properties and elements should also be recursively made readonly, all the way down.

Key Requirements:

  1. The DeepReadonly<T> type must make all direct properties of T readonly.
  2. If a property of T is an object, its properties must also be recursively made readonly.
  3. If a property of T is an array, its elements must be recursively made readonly.
  4. The utility type should handle primitive types gracefully (i.e., they remain as they are, as primitives are inherently immutable).
  5. It should correctly handle nested arrays and arrays of objects.

Expected Behavior:

Given a type T, DeepReadonly<T> should produce a type where attempting to modify any property or array element will result in a TypeScript compilation error.

Edge Cases:

  • Handling of null and undefined values within objects and arrays.
  • Correctly applying readonly to arrays of primitive types.
  • Correctly applying readonly to arrays of complex object types.

Examples

Example 1:

interface User {
  id: number;
  name: string;
  address: {
    street: string;
    city: string;
    zip: number;
  };
  roles: string[];
}

type DeepReadonlyUser = DeepReadonly<User>;

// Expected behavior:
// const readonlyUser: DeepReadonlyUser = { ... };
// readonlyUser.id = 10; // Error: Cannot assign to 'id' because it is a read-only property.
// readonlyUser.address.street = "New Street"; // Error: Cannot assign to 'street' because it is a read-only property.
// readonlyUser.roles.push("admin"); // Error: Property 'push' does not exist on type 'readonly string[]'.

Example 2:

type Data = {
  items: {
    name: string;
    value: number;
    nested: {
      flag: boolean;
    }[];
  }[];
  config: {
    timeout: number;
  };
  tags: Array<string | null>;
};

type DeepReadonlyData = DeepReadonly<Data>;

// Expected behavior:
// const readonlyData: DeepReadonlyData = { ... };
// readonlyData.items[0].name = "New Name"; // Error
// readonlyData.items[0].nested[0].flag = false; // Error
// readonlyData.config.timeout = 5000; // Error
// readonlyData.tags.push("new"); // Error
// readonlyData.tags[0] = "new"; // Error (if original was string, not null)

Example 3:

type Primitives = {
  a: number;
  b: string;
  c: boolean;
  d: null;
  e: undefined;
};

type DeepReadonlyPrimitives = DeepReadonly<Primitives>;

// Expected behavior:
// const readonlyPrimitives: DeepReadonlyPrimitives = { ... };
// readonlyPrimitives.a = 10; // Error
// readonlyPrimitives.d = "not null"; // Error

Constraints

  • The DeepReadonly<T> utility type must be defined purely using TypeScript's type system (no runtime code).
  • It should not rely on any external libraries.
  • The solution should be efficient in terms of type checking performance, avoiding overly complex conditional types where possible.

Notes

Consider how to handle different kinds of types, including primitives, objects, arrays, and potentially ReadonlyArray. You might find it useful to leverage mapped types and conditional types. Remember that readonly applied to an array makes its elements readonly, but its methods (like push, pop) are still accessible. You need to ensure that even array methods that modify the array are inaccessible on the resulting type.

Loading editor...
typescript