Hone logo
Hone
Problems

Implementing Advanced Typeof Utilities in TypeScript

TypeScript's built-in typeof operator is useful for basic type checking. However, for more complex scenarios, we often need to extract or manipulate types in more sophisticated ways. This challenge will guide you through implementing several common "utility types" that extend the power of TypeScript's type system. Mastering these will significantly improve your ability to write robust and expressive TypeScript code.

Problem Description

Your task is to implement three common TypeScript utility types: DeepReadonly<T>, Mutable<T>, and DeepMutable<T>.

  1. DeepReadonly<T>: This utility type should recursively make all properties of an object type, and all its nested object properties, readonly.
  2. Mutable<T>: This utility type should recursively remove the readonly modifier from all properties of an object type.
  3. DeepMutable<T>: This utility type should recursively remove the readonly modifier from all properties of an object type, including those in nested objects.

Key Requirements:

  • The implementations must be generic utility types.
  • They should handle primitive types, arrays, objects, and nested structures correctly.
  • For DeepReadonly, the readonly modifier should be applied to all levels of nested objects.
  • For Mutable and DeepMutable, the readonly modifier should be removed at all levels. Mutable should only affect top-level properties, while DeepMutable should affect all nested properties as well.

Expected Behavior:

  • DeepReadonly should turn string into readonly string, number into readonly number, etc. For objects, it should make all properties readonly, and if a property is itself an object, it should also apply DeepReadonly recursively.
  • Mutable should revert top-level readonly properties back to writable.
  • DeepMutable should revert all readonly properties, at any depth, back to writable.

Edge Cases:

  • Consider how these utilities should behave with primitive types (strings, numbers, booleans, null, undefined, symbols, bigints).
  • Consider how they should behave with array types (e.g., number[]). The readonly modifier should be applied/removed to the array itself and its elements if they are objects.
  • Consider interfaces and type aliases.

Examples

Example 1: DeepReadonly<T>

interface User {
    name: string;
    address: {
        street: string;
        city: string;
        zipCode: number;
    };
    hobbies: string[];
}

type ReadonlyUser = DeepReadonly<User>;

// Expected type for ReadonlyUser:
// {
//     readonly name: string;
//     readonly address: {
//         readonly street: string;
//         readonly city: string;
//         readonly zipCode: number;
//     };
//     readonly hobbies: readonly string[]; // or hobbies: string[] if only object properties are recursed
// }
// Note: The exact handling of array elements might vary slightly based on interpretation.
// For this challenge, assume readonly is applied to the array itself and its object elements.

let user: ReadonlyUser = {
    name: "Alice",
    address: {
        street: "123 Main St",
        city: "Anytown",
        zipCode: 12345
    },
    hobbies: ["reading", "hiking"]
};

// user.name = "Bob"; // Error: Cannot assign to 'name' because it is a read-only property.
// user.address.city = "Othertown"; // Error: Cannot assign to 'city' because it is a read-only property.
// user.hobbies.push("swimming"); // Error: Property 'push' does not exist on type 'readonly string[]'.

Example 2: Mutable<T> and DeepMutable<T>

interface Product {
    readonly id: number;
    name: string;
    details: {
        readonly description: string;
        price: number;
    };
}

type MutableProduct = Mutable<Product>;
// Expected type for MutableProduct:
// {
//     id: number; // Top-level readonly removed
//     name: string;
//     details: {
//         readonly description: string; // Nested readonly remains
//         price: number;
//     };
// }

type DeepMutableProduct = DeepMutable<Product>;
// Expected type for DeepMutableProduct:
// {
//     id: number;
//     name: string;
//     details: {
//         description: string; // Nested readonly removed
//         price: number;
//     };
// }

let product: Product = {
    id: 101,
    name: "Laptop",
    details: {
        description: "Powerful computing device",
        price: 1200
    }
};

let mutableProd: MutableProduct = product;
mutableProd.id = 102; // OK

// mutableProd.details.description = "Ultra thin"; // Error: Cannot assign to 'description' because it is a read-only property.

let deepMutableProd: DeepMutableProduct = product;
deepMutableProd.id = 103; // OK
deepMutableProd.details.description = "Ultra thin"; // OK

Example 3: Array and Primitive Handling

type ReadonlyArrayExample = DeepReadonly<number[]>;
// Expected: readonly number[]

type MutableArrayExample = Mutable<readonly string[]>;
// Expected: string[]

type DeepMutableArrayExample = DeepMutable<{ items: readonly (number | { readonly value: number })[] }>;
// Expected: { items: (number | { value: number })[] }

Constraints

  • Your solution must be written entirely in TypeScript.
  • You should use conditional types and mapped types to achieve the implementations.
  • Avoid using any external libraries or pre-built utility types from ts-essentials or similar.
  • The types should be performant enough for typical use cases within a TypeScript project.

Notes

  • Recall how to make properties readonly in TypeScript using the readonly modifier.
  • Think about how to iterate over the properties of an object type using mapped types.
  • Consider how to conditionally apply or remove modifiers based on whether a property is readonly.
  • For recursion, you'll need to check if a property's type is itself an object or array and then apply the utility type to that nested type.
  • The keyof T operator will be crucial for iterating over object keys.
  • The T[K] syntax is used to access the type of a property.
  • Be mindful of the difference between making an array readonly and making its elements readonly.
  • Consider using infer within conditional types for more complex type manipulations, especially with arrays.
  • Remember that primitive types (string, number, boolean, etc.) and functions are generally not affected by readonly or mutable transformations in the same way as object properties. Your types should handle these gracefully.
Loading editor...
typescript