TL;DR: The Quick Fix
Clone the array using the spread operator to create a mutable version. It takes one line of code:
const readonlyArray: readonly string[] = ['debug', 'info', 'error'];
// Create a mutable copy
processData([...readonlyArray]);
For a more permanent fix, change your function signature to accept readonly string[]. This prevents unnecessary memory usage from copying arrays.
The 2 AM Production Scenario
You're deploying a hotfix. You pull configuration data from a source like Zod, Prisma, or a constant defined with as const. You pass this data into a utility function you've used a hundred times. Suddenly, the build fails.
Argument of type 'readonly string[]' is not assignable to parameter of type 'string[]'.
The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.
TypeScript is being a strict bodyguard here. You have an array that promised never to change. Now, you're trying to hand it to a function that has the power to mutate it. TypeScript won't let that risk slide.
The Root Cause: Contract Mismatches
In TypeScript, string[] is just shorthand for Array<string>. This type includes methods that change the data in place, such as .push(), .sort(), or .splice().
The readonly string[] type is different. It explicitly strips away those mutation methods. If TypeScript let you pass a readonly array into a function expecting a standard string[], that function could call .push('new-item'). That would break the immutability contract of your original data. This often leads to runtime bugs that are a nightmare to debug in large state-managed apps.
Fix Approaches
1. Update the Function Signature (Recommended)
Check if your function actually changes the array. If it only reads data—like using .map(), .find(), or checking the .length—change the parameter type. This is the cleanest architectural move.
// Change this:
function logMessages(messages: string[]) {
messages.forEach(m => console.log(m));
}
// To this:
function logMessages(messages: readonly string[]) {
messages.forEach(m => console.log(m));
}
This makes your function more versatile. It can now handle both mutable and immutable arrays without complaint.
2. Create a Mutable Copy
Sometimes you're stuck. You might be using a legacy third-party library that you can't edit. If the function needs to .sort() the data or you simply can't change the type, create a shallow copy.
const config: readonly string[] = ['admin', 'user'];
// Using spread operator
someExternalLibraryFunction([...config]);
// Or using .slice() for older environments
someExternalLibraryFunction(config.slice());
Warning: Spreading only creates a shallow copy. If your array contains 1,000 objects, the new array still points to those same 1,000 objects in memory. Modifying an object's property inside the function will still affect the original data.
3. Type Assertion (The Escape Hatch)
Use this only if you are 100% sure the receiving function won't touch the data. It tells the compiler to "trust me," which bypasses the safety checks you're using TypeScript for in the first place.
const data: readonly string[] = fetchData();
// Force treat as mutable
handleData(data as string[]);
Verification Steps
Confirm the fix works across your pipeline:
- Compiler Check: Run
npx tsc --noEmit. If the terminal stays clear, the type error is resolved. - Memory Audit: If you are copying an array with 50,000+ items inside a loop, monitor memory usage. Signature updates (Option 1) are always better for performance.
- Logic Test: If you updated a function to
readonly, check if it uses.sort(). Since.sort()mutates in place, TypeScript will now throw a new error inside that function, forcing you to use.toSorted()or a copy.

