Unlocking Mutability in TypeScript: How to Remove readonly from Types

Unlocking Mutability in TypeScript: How to Remove readonly from Types
Photo by THE 9TH Coworking / Unsplash

TypeScript is powerful for enforcing type safety, and one of its most useful features is the ability to express immutability through the readonly modifier. However, there are times when we want to reverse that behavior — making a type's properties mutable again. That's where the Mutable<T> utility type comes in.

Let’s explore how to use it effectively, and why it’s such a handy trick for TypeScript developers.


The Problem: Readonly Types

Imagine you’re working with a data structure where all fields are marked as readonly. This is great for safety — it prevents accidental mutation. But what if, at some point, you need to modify that data?

Here’s an example:

type Example = {
  readonly a: string;
  readonly b: number;
};

In this case, a and b are locked down — any attempt to change them results in a compile-time error.


The Solution: Mutable<T>

To solve this, you can define a mapped type that strips away the readonly modifier from every property.

type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

What’s Happening Here?

  • keyof T: Gets all keys from the type T.
  • [K in keyof T]: Iterates over each key.
  • T[K]: Fetches the corresponding value type.
  • -readonly: This is the magic — it removes the readonly attribute from each key.

Now, if we apply it to our example:

type Result = Mutable<Example>;

The resulting type is:

type Result = {
  a: string;
  b: number;
};

And now you can freely assign new values to a and b.


Why This Is Useful

  • Refactoring: Sometimes types change over time. You might start with immutable data but need to make updates later.
  • Utility Functions: You may want to write a utility that manipulates data and don’t want TypeScript blocking you with readonly warnings.
  • Interoperability: Working with third-party types that are readonly, but your logic requires mutable properties.

Other Considerations

1. Deep vs. Shallow Mutability

This Mutable<T> implementation is shallow — it only removes readonly from the top-level properties. If a property is itself an object with readonly fields, those are untouched.

Example:

type NestedExample = {
  readonly a: {
    readonly x: string;
  };
};

Using Mutable<NestedExample> will still leave a.x as readonly.

To handle deep mutability, you need a recursive version:

type DeepMutable<T> = {
  -readonly [K in keyof T]: T[K] extends object
    ? DeepMutable<T[K]>
    : T[K];
};
Caution: Recursive types can be powerful but must be used with care to avoid unexpected behavior, especially with arrays and complex union types.

2. Intentionality Matters

Mutability is sometimes discouraged in large codebases because it can lead to unintended side effects. Use Mutable<T> only when there's a clear and justified need to make a value changeable.


3. Interop with Libraries

When using libraries like Redux or Immer that rely on immutability, you’ll often need to preserve readonly properties. Avoid using Mutable<T> in these contexts unless you’re intentionally stepping outside the pattern.


Finally

The Mutable<T> utility is a powerful technique to keep in your TypeScript toolbox. It’s especially useful when dealing with strict types that need to become flexible for valid reasons.

By understanding how mapped types and modifiers like -readonly work, you gain fine-grained control over your types — enabling cleaner, more intentional code.

So the next time you find yourself battling with immutability, remember: you can take control back — thoughtfully and safely — using a simple yet powerful utility.

Support Us