Hone logo
Hone
Problems

TypeScript as const Helper Utilities

Many developers leverage TypeScript's as const assertion to create read-only, deeply immutable data structures. However, applying as const to complex objects or arrays manually can be tedious and error-prone. This challenge focuses on creating utility types that automatically infer and apply as const semantics to your types.

Problem Description

Your task is to create a set of TypeScript utility types that can be used to transform existing types into their as const equivalents. Specifically, you need to create:

  1. Const<T>: A type that takes any type T and makes it deeply read-only, similar to how as const works on literals. This means properties of objects become readonly and array elements become readonly.
  2. ConstObject<T>: A type that specifically takes an object type T and makes all its properties readonly.
  3. ConstArray<T>: A type that specifically takes an array type T and makes its elements readonly.

These helpers should mimic the behavior of as const by recursively applying read-only properties and literal types where appropriate.

Examples

Example 1: Basic Object Transformation

// Input Type
type MyObject = {
  name: string;
  age: number;
  address: {
    street: string;
    city: string;
  };
};

// Applying the helper
type ConstMyObject = Const<MyObject>;

/*
Expected Output (represented as a type):
{
  readonly name: "string"; // Note: string literal types are inferred
  readonly age: number;
  readonly address: {
    readonly street: "string";
    readonly city: "string";
  };
}
*/

// When you try to assign a mutable value to a ConstMyObject, it should be a type error.
// let mutableName: ConstMyObject['name'] = "Alice"; // Error: Type 'string' is not assignable to type '"string"'.

Explanation: The Const<T> type should recursively traverse the MyObject type. It should make all properties readonly. String literals should be inferred where applicable (e.g., name becoming "string").

Example 2: Basic Array Transformation

// Input Type
type MyArray = string[];

// Applying the helper
type ConstMyArray = Const<MyArray>;

/*
Expected Output (represented as a type):
readonly "string"[]
*/

// let mutableElement: ConstMyArray[0] = "hello"; // Error: Type 'string' is not assignable to type '"string"'.

Explanation: The Const<T> type should recognize MyArray as an array and make its elements read-only.

Example 3: Mixed Types and Specific Helpers

// Input Type
type ComplexData = {
  id: number;
  tags: string[];
  settings: {
    theme: "dark" | "light";
    notifications: boolean;
  };
  isActive: boolean;
};

// Using specific helpers
type ConstArrayTags = ConstArray<ComplexData['tags']>;
type ConstObjectSettings = ConstObject<ComplexData['settings']>;
type FullyConst = Const<ComplexData>;

/*
Expected Output for ConstArrayTags:
readonly "string"[]

Expected Output for ConstObjectSettings:
{
  readonly theme: "dark" | "light";
  readonly notifications: false; // boolean literal false inferred
}

Expected Output for FullyConst:
{
  readonly id: number;
  readonly tags: readonly "string"[];
  readonly settings: {
    readonly theme: "dark" | "light";
    readonly notifications: false;
  };
  readonly isActive: false;
}
*/

Explanation: This example demonstrates how Const<T> should handle nested structures, unions, and primitives. It also shows the usage of the more specific ConstObject and ConstArray helpers. Note how isActive: false and notifications: false are inferred as literal types because their original type was boolean, and as const would infer false if the value was actually false. Your helpers should aim to infer these literal types when possible.

Constraints

  • Your solution must be written entirely in TypeScript.
  • The utility types must be generic and work with any valid TypeScript type.
  • The solution should achieve deep immutability, meaning nested objects and arrays are also made read-only.
  • Avoid using external libraries or packages.

Notes

  • Consider how TypeScript's readonly modifier works on object properties.
  • Think about how to handle array types and their element types.
  • For primitives and unions, as const often infers specific literal types. Your Const<T> helper should try to replicate this behavior. For example, if T is string, Const<T> should infer "string". If T is boolean and the original value was false, it should infer false.
  • You might need to use mapped types and conditional types to achieve the recursive behavior.
  • The specific helpers (ConstObject, ConstArray) are to ensure you understand how to target specific structures, but the primary goal is the comprehensive Const<T> type.
Loading editor...
typescript