CodeFixesHub
    programming tutorial

    Using Readonly<T>: Making All Properties Immutable

    Enforce immutability with Readonly<T>: practical patterns, deep-readonly, runtime tips, and examples. Learn best practices and avoid bugs — read the guide now.

    article details

    Quick Overview

    TypeScript
    Category
    Sep 22
    Published
    18
    Min Read
    2K
    Words
    article summary

    Enforce immutability with Readonly<T>: practical patterns, deep-readonly, runtime tips, and examples. Learn best practices and avoid bugs — read the guide now.

    Using Readonly: Making All Properties Immutable

    Introduction

    Mutable state is a frequent source of bugs in JavaScript and TypeScript applications: properties get changed in unexpected places, objects drift from their intended shapes, and reasoning about data flow becomes harder. TypeScript's Readonly utility type is a compact, powerful tool to express a contract that an object's top-level properties should not be reassigned. In this tutorial you'll learn when and how to use Readonly, how it differs from readonly modifiers and const, how to build deep immutability patterns, how to integrate immutability with APIs and runtime validation, and strategies to avoid common traps.

    This article is aimed at intermediate developers who already know TypeScript basics and want to apply immutability to reduce bugs and create clearer APIs. We'll cover practical examples, step-by-step transformations, interactions with other utility types, best practices for libraries and applications, and debugging tips. By the end you'll be able to choose between Readonly, readonly properties, and runtime enforcement, create reusable mapped types for deep readonly, and use immutability as part of robust API typing and validation.

    Background & Context

    Readonly is one of TypeScript's built-in utility types that transforms an object type by making each of its properties readonly. It is shorthand for a mapped type that prefixes each property with the readonly modifier. This covers compile-time immutability for top-level properties only — it does not freeze nested objects at runtime or make arrays immutable unless you compose it with readonly arrays or other techniques.

    Understanding Readonly is important because it helps you define safer public APIs and reduce accidental state mutation in your codebase. It's frequently used in library typings, function signatures that promise not to change inputs, and shared model definitions. To use immutability effectively you also need to understand related concepts like Partial, mapped types, and runtime validation to avoid false assumptions about deep immutability.

    Key Takeaways

    • Readonly makes all top-level properties of a type readonly at compile time.
    • Readonly is a mapped type and can be composed with other utilities like Partial and Pick.
    • Readonly does not deeply freeze nested objects at runtime — for deep immutability you need mapped types or runtime freezing.
    • Prefer immutable types on public API surfaces and function parameters to communicate intent.
    • Use runtime validation (Zod/Yup) or Object.freeze when you need runtime guarantees.

    Prerequisites & Setup

    You should be familiar with TypeScript basics: interfaces, types, generics, and mapped types. A recent TypeScript version (4.x+) is recommended to access improved type inference and nicer error messages. To try examples locally create a project with TypeScript installed (npm i -D typescript) and configure tsconfig.json (strict mode recommended). We'll include compilation examples and runtime snippets — optional libraries for runtime validation are noted in the sections below.

    If you want a refresher on utility types before diving in, check the utility types guide.

    Main Tutorial Sections

    What Readonly Does (and What It Doesn't)

    Readonly transforms a type's properties to readonly, e.g.:

    ts
    type User = { id: number; name: string };
    type ImmutableUser = Readonly<User>;
    
    const u: ImmutableUser = { id: 1, name: 'Jane' };
    // u.id = 2; // Error: cannot assign to 'id' because it is a read-only property

    Important: Readonly is compile-time only; it does not call Object.freeze on runtime values. If you need runtime protection you must combine it with Object.freeze or a validation layer. For comparisons to other utilities like Partial<T), see our deep dive on Partial.

    Readonly vs readonly Modifier in Interfaces

    You can declare readonly directly in interfaces and classes:

    ts
    interface Point { readonly x: number; readonly y: number }

    Readonly is useful when you need to transform an existing type without re-declaring every field. For example, you can write a function that returns a Readonly without changing the original interface. If you author library types, sometimes it's cleaner to expose a Readonly variant rather than duplicate definitions. When designing APIs, combine this with patterns in Generic Interfaces to keep interfaces flexible while controlling mutability.

    Readonly with Arrays and Tuples

    Arrays have a special readonly array type: readonly T[] or ReadonlyArray:

    ts
    const arr: ReadonlyArray<number> = [1, 2, 3];
    // arr.push(4); // Error
    
    const tuple: readonly [string, number] = ['a', 1];
    // tuple[0] = 'b'; // Error

    Use readonly arrays to prevent mutation of list containers; that complements Readonly for object shapes. When working with data structures returned from functions consider returning readonly arrays to communicate immutability.

    Combining Readonly with Other Utility Types

    Readonly composes well with Pick, Omit, and Partial. For example, make some optional fields readonly:

    ts
    type Settings = { host: string; port?: number; timeout?: number };
    type StableSettings = Readonly<Pick<Settings, 'host'>> & Partial<Pick<Settings, 'port' | 'timeout'>>;

    If you need a refresher on utility type composition and transformations, see the comprehensive utility types guide and how Partial works in practice in Using Partial: Making All Properties Optional.

    Creating a DeepReadonly Type

    Readonly is shallow. To make nested properties readonly you can write a mapped conditional type:

    ts
    type DeepReadonly<T> = T extends Function
      ? T
      : T extends Array<infer U>
      ? ReadonlyArray<DeepReadonly<U>>
      : T extends object
      ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
      : T;
    
    type Nested = { a: { b: number[] }, f(): void };
    type RN = DeepReadonly<Nested>;

    This preserves functions (so methods stay callable) and recursively converts arrays and objects. Use this pattern when you need type-level guarantees across an entire tree.

    Readonly in Function Signatures

    Marking function parameters as Readonly communicates intent:

    ts
    function processUser(user: Readonly<User>) {
      // compile-time guarantee: we won't reassign user's properties
    }

    If your function needs to transform data, prefer returning a new object rather than mutating inputs. Combine Readonly with generic helpers when writing reusable APIs — see patterns in Generic Functions.

    Readonly in Generic Types, Interfaces and Classes

    When writing generic types, you can expose readonly variants:

    ts
    interface Container<T> { value: T }
    
    type ReadonlyContainer<T> = Readonly<Container<T>>;

    For class-based libraries, readonly properties can be declared on class fields. If you're authoring classes with generics, look at Generic Classes and Generic Interfaces for patterns on exposing typed, immutable APIs.

    Runtime Guarantees: Object.freeze and Validation

    TypeScript's immutability is compile-time. If you need runtime enforcement, use Object.freeze or validation libraries. Example:

    ts
    const frozenUser = Object.freeze({ id: 1, name: 'Jane' } as const);
    // frozenUser.id = 2; // fails silently in non-strict mode or throws in strict mode

    For robust runtime guarantees and consistent parsing, integrate validation with libraries like Zod or Yup. See practical integration patterns in Using Zod or Yup for Runtime Validation with TypeScript Types. For API payloads, immutability plus parsing can prevent accidental mutation of request/response objects — learn more at Typing API Request and Response Payloads with Strictness.

    Avoiding Unsafe Type Assertions with Readonly

    Avoid asserting types to circumvent read-only checks. Using as or <> to cast can break immutability guarantees:

    ts
    const r: Readonly<User> = { id: 1, name: 'Jane' };
    const writable = r as unknown as User; // bypasses readonly — dangerous

    If you must convert, do so with explicit copies:

    ts
    const writableCopy = { ...r };
    writableCopy.name = 'John'; // safe explicit mutation on a new object

    Read more about the risks of type assertions in our guide: Type Assertions (as keyword or <>) and Their Risks.

    Interop with Third-Party Libraries and Typings

    When a library exposes mutable types, you can wrap them with Readonly to protect your internal code. However, if the library mutates objects internally you must coordinate: either make defensive copies on input or use runtime freezing. When writing library types that export immutability, document whether your types are shallow or deep and prefer returning new states rather than mutating inputs. For patterns on typing different library shapes consult articles on typing libraries with complex signatures and class-based libraries such as Typing Libraries With Complex Generic Signatures — Practical Patterns and Typing Libraries That Are Primarily Class-Based in TypeScript.

    Advanced Techniques

    • Create specialized mapped types: beyond DeepReadonly, build DeepMutable or SelectiveReadonly<T, K>. Use conditional types and infer to preserve function types and handle tuples correctly.
    • Use branded types with readonly to avoid accidental mixing of mutable and immutable variants.
    • Combine Readonly with opaque types or TypeScript's nominal typing patterns to surface intent in APIs.
    • For performance-sensitive code, avoid deep freezing large objects at runtime; validate only the public boundary or use structural checks. If designing a library API, consider offering both a frozen runtime variant and a typed Readonly variant to balance safety and performance.

    Best Practices & Common Pitfalls

    Dos:

    • Use Readonly on public API inputs/outputs to communicate no-mutation intent.
    • Return new objects instead of mutating inputs; prefer pure functions.
    • Compose Readonly with Partial, Pick, and other utilities for precise contracts.

    Don'ts:

    • Don't assume Readonly is deep — implement DeepReadonly when needed.
    • Avoid circumventing readonly via type assertions or any casts.
    • Don't overuse deep freezing at runtime for large nested structures — consider strategic validation.

    Troubleshooting:

    • If TypeScript still allows a mutation, check for as any or as unknown as casts in your codebase.
    • Watch out when interacting with libraries that mutate objects — copy inputs defensively.
    • Use tsconfig strict options to surface unintended mutability early.

    Real-World Applications

    Conclusion & Next Steps

    Readonly is a compact, expressive tool to declare top-level immutability in TypeScript types. Combined with mapped types, defensive copying, and optional runtime validation, it helps you write safer APIs and reduce mutation-related bugs. Next, practice converting common mutable types in a codebase to readonly variants, explore DeepReadonly patterns, and integrate runtime validators to enforce guarantees in production.

    For related topics, revisit utility types and explore generic programming patterns in Introduction to Utility Types: Transforming Existing Types and Generic Functions.

    Enhanced FAQ

    Q1: Does Readonly make nested objects immutable at runtime?

    A1: No. Readonly only affects TypeScript's type system and is shallow: it marks the top-level properties as read-only at compile time. Nested objects remain mutable at runtime unless you use a deep mapped type like DeepReadonly for type-level immutability and Object.freeze or runtime validation for runtime immutability.

    Q2: How is Readonly implemented under the hood?

    A2: Readonly is a mapped type equivalent to { readonly [P in keyof T]: T[P] }. It's a compile-time transform that changes property modifiers in the type system. It doesn't emit runtime code. You can extend or customize this pattern to create DeepReadonly and selective readonly variations.

    Q3: When should I use readonly fields in interfaces vs Readonly?

    A3: Use readonly fields directly when authoring the canonical type. Use Readonly when you want to transform an existing type without changing its declaration, or when providing readonly variants (e.g., an API that returns a readonly view of a mutable internal type). For library design, choose one convention and document it clearly.

    Q4: Can I convert a Readonly back to a mutable type?

    A4: Not directly via the type system without a cast. You can create a mutable copy at runtime: const copy = { ...readonlyObj }; which yields a new object with writable properties. Type-level conversion would require a mapped Mutable type, but converting in code often indicates you should return a new object rather than mutate.

    Q5: Is readonly the same as const?

    A5: No. const applies to variables (bindings) and means the variable cannot be reassigned. readonly applies to object properties preventing reassignment of that property on the object. const does not make objects immutable — their properties are still mutable unless declared readonly at the type level or frozen at runtime.

    Q6: How do I handle arrays with Readonly?

    A6: For arrays, use ReadonlyArray or the shorthand readonly T[]. That prevents mutation methods like push/pop. Combine ReadonlyArray with readonly tuples for fixed shapes. For nested arrays in complex types use DeepReadonly to recursively convert array elements.

    Q7: Should I use Object.freeze to enforce immutability at runtime?

    A7: Object.freeze offers runtime guarantees for shallow immutability. It can be useful at public boundaries if you want to prevent consumer mutation. But freeze can have performance implications on large objects and doesn't deeply freeze nested objects unless you recursively freeze them. For robust parsing and validation consider schema validators like Zod or Yup as described in Using Zod or Yup for Runtime Validation with TypeScript Types.

    Q8: How does Readonly interact with generic APIs?

    A8: Readonly composes well with generics. You can design functions and interfaces that accept or return Readonly to express no-mutation guarantees. For reusable patterns, see examples of Generic Interfaces and Generic Functions to learn idiomatic approaches.

    Q9: What are common pitfalls when using Readonly?

    A9: Common pitfalls include assuming deep immutability, bypassing readonly with type assertions (as), and not accounting for library internals that mutate objects. Always prefer explicit copies over silent casts, and complement type-level readonly with runtime strategies where necessary.

    Q10: How do I apply immutability to API payloads in a server or client?

    A10: For API payloads, validate incoming data and then convert it into readonly shapes (either via types + runtime freezing or by returning copies with readonly types). Combining typed validation and readonly types helps ensure that downstream logic treats payloads as immutable — see Typing API Request and Response Payloads with Strictness for patterns and examples.


    If you want practical exercises next, try converting a small module of mutable models in your codebase to readonly typed variants, implement DeepReadonly, and add a simple Zod schema to freeze/validate runtime data. For more advanced library typing patterns see Typing Libraries With Complex Generic Signatures — Practical Patterns and remember to balance developer ergonomics with safety.

    article completed

    Great Work!

    You've successfully completed this TypeScript tutorial. Ready to explore more concepts and enhance your development skills?

    share this article

    Found This Helpful?

    Share this TypeScript tutorial with your network and help other developers learn!

    continue learning

    Related Articles

    Discover more programming tutorials and solutions related to this topic.

    No related articles found.

    Try browsing our categories for more content.

    Content Sync Status
    Offline
    Changes: 0
    Last sync: 11:19:56 PM
    Next sync: 60s
    Loading CodeFixesHub...