Unlocking Mutability in TypeScript: How to Remove readonly from Types
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 typeT
.[K in keyof T]
: Iterates over each key.T[K]
: Fetches the corresponding value type.-readonly
: This is the magic — it removes thereadonly
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.
Comments ()