Implementing Readonly Utilities in TypeScript
TypeScript's utility types are powerful tools for manipulating and transforming types. This challenge focuses on creating your own Readonly utility type, which makes all properties of a type readonly. This is useful for enforcing immutability and preventing accidental modification of objects, leading to more robust and predictable code.
Problem Description
You are tasked with implementing a Readonly utility type in TypeScript. This type should take a type as input and return a new type where all properties of the input type are marked as readonly. The utility type should handle both object types and primitive types gracefully. If the input type is already a primitive (like string, number, boolean), the output type should be the same primitive type. It should also correctly handle union types and intersection types.
Key Requirements:
- Object Types: For object types, all properties should be
readonly. - Primitive Types: For primitive types (string, number, boolean, symbol, bigint, null, undefined), the output type should be the same as the input type.
- Union Types: If the input is a union type, the output should be a union of the readonly versions of each member of the union.
- Intersection Types: If the input is an intersection type, the output should be an intersection of the readonly versions of each member of the intersection.
- Mapped Types: Utilize mapped types to iterate over the properties of the input type.
- Conditional Types: Use conditional types to handle primitive types and ensure correct behavior.
Expected Behavior:
The Readonly<Type> utility type should produce the following results:
Readonly<{ name: string; age: number; }>should be equivalent to{ readonly name: string; readonly age: number; }Readonly<string>should be equivalent tostringReadonly<string | number>should be equivalent toreadonly string | readonly numberReadonly<{ a: string } & { b: number }>should be equivalent to{ readonly a: string; readonly b: number; }
Edge Cases to Consider:
- Types with index signatures (e.g.,
{[key: string]: number}) - Optional properties (e.g.,
name?: string) - Null and undefined types
- Type aliases and interfaces
Examples
Example 1:
type MyType = {
name: string;
age: number;
address?: string;
};
type ReadonlyMyType = Readonly<MyType>;
// ReadonlyMyType should be:
// {
// readonly name: string;
// readonly age: number;
// readonly address?: string;
// }
Explanation: All properties of MyType are made readonly. The optional property address is also made readonly.
Example 2:
type StringOrNumber = string | number;
type ReadonlyStringOrNumber = Readonly<StringOrNumber>;
// ReadonlyStringOrNumber should be:
// readonly string | readonly number
Explanation: The union type is handled correctly, creating a union of readonly string and readonly number.
Example 3:
type IntersectionType = { a: string } & { b: number };
type ReadonlyIntersectionType = Readonly<IntersectionType>;
// ReadonlyIntersectionType should be:
// {
// readonly a: string;
// readonly b: number;
// }
Explanation: The intersection type is handled correctly, creating an intersection of readonly string and readonly number.
Constraints
- The solution must be written in TypeScript.
- The solution must correctly handle all the cases described in the Problem Description and Examples.
- The solution should be reasonably efficient. While performance is not the primary concern, avoid unnecessarily complex or inefficient type manipulations.
- The solution should be well-formatted and readable.
Notes
- This problem requires a good understanding of TypeScript's utility types, mapped types, and conditional types.
- Start by handling the object type case and then progressively add support for primitive types, union types, and intersection types.
- Consider using a recursive approach to handle nested object types.
- Think about how to handle index signatures – you might choose to simply pass them through unchanged, or apply a
readonlyconstraint to the index signature's type. Document your choice.