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>.
DeepReadonly<T>: This utility type should recursively make all properties of an object type, and all its nested object properties, readonly.Mutable<T>: This utility type should recursively remove thereadonlymodifier from all properties of an object type.DeepMutable<T>: This utility type should recursively remove thereadonlymodifier 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, thereadonlymodifier should be applied to all levels of nested objects. - For
MutableandDeepMutable, thereadonlymodifier should be removed at all levels.Mutableshould only affect top-level properties, whileDeepMutableshould affect all nested properties as well.
Expected Behavior:
DeepReadonlyshould turnstringintoreadonly string,numberintoreadonly number, etc. For objects, it should make all properties readonly, and if a property is itself an object, it should also applyDeepReadonlyrecursively.Mutableshould revert top-level readonly properties back to writable.DeepMutableshould 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[]). Thereadonlymodifier 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-essentialsor 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
readonlymodifier. - 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 Toperator 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
readonlyand making its elementsreadonly. - Consider using
inferwithin 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
readonlyormutabletransformations in the same way as object properties. Your types should handle these gracefully.