TypeScript Readonly Deep Modifier
This challenge focuses on implementing a TypeScript utility type that recursively makes all properties of a given type, including nested objects and arrays, readonly. This is crucial for ensuring data immutability, preventing accidental modifications in complex data structures, and improving code safety.
Problem Description
You need to create a TypeScript generic utility type called DeepReadonly<T>. This type should take a type T and return a new type where:
- All properties of
Tare marked asreadonly. - If a property's value is an object (but not
null), its properties should also be recursively marked asreadonlyusingDeepReadonly. - If a property's value is an array, the array itself should be marked as
readonly, and its elements should also be recursively marked asreadonlyusingDeepReadonly.
Key Requirements:
- The
DeepReadonly<T>type must handle primitive types, objects, arrays, andnullvalues correctly. - The recursion should apply to nested objects and array elements.
- The resulting type should effectively prevent any mutation of the original data structure.
Expected Behavior:
Given an input type, DeepReadonly<T> should produce a type where all properties are readonly, and any nested objects or array elements are also deeply readonly.
Edge Cases to Consider:
nullvalues:nullshould remainnulland not be treated as an object that needs deeper readonly conversion.- Arrays of primitives: The array itself should be readonly, and the primitive elements within should also be treated as readonly (though primitives are inherently immutable).
- Arrays of objects: Both the array and the objects within the array should be deeply readonly.
- Readonly properties in the original type: These should remain readonly.
- Optional properties: These should still be optional but readonly.
Examples
Example 1:
Input Type: {
name: string;
age: number;
address: {
street: string;
city: string;
};
}
Output Type: {
readonly name: string;
readonly age: number;
readonly address: {
readonly street: string;
readonly city: string;
};
}
Explanation: All properties, including those in the nested 'address' object, are made readonly.
Example 2:
Input Type: {
items: Array<{ id: number; value: string }>;
isActive: boolean;
}
Output Type: {
readonly items: ReadonlyArray<{ readonly id: number; readonly value: string }>;
readonly isActive: boolean;
}
Explanation: The 'items' array is marked as ReadonlyArray, and its object elements are also deeply readonly. 'isActive' is a primitive and becomes readonly.
Example 3:
Input Type: {
data: {
users: Array<{ name: string; settings?: { theme: string } }>;
config: null | { retries: number };
};
version: 1;
}
Output Type: {
readonly data: {
readonly users: ReadonlyArray<{ readonly name: string; readonly settings?: { readonly theme: string } }>;
readonly config: null | { readonly retries: number };
};
readonly version: 1;
}
Explanation: This example demonstrates handling optional properties within arrays, null values, and primitive union types.
Constraints
- The solution must be a single generic TypeScript utility type
DeepReadonly<T>. - The type must be fully implemented using TypeScript's type system. No runtime JavaScript code is expected.
- The solution should be efficient in terms of type checking complexity.
Notes
- Consider how to distinguish between objects that should be recursed into and other types like
nullor primitives. - The
ReadonlyArray<T>built-in utility type will be useful for handling arrays. - Think about how to handle union types, especially those involving
null. - The goal is to create a type that provides compile-time immutability guarantees.