How to fix Parameter decorator cannot use 'readonly' modifier in TypeScript
This TypeScript error occurs when you try to use the 'readonly' modifier on a parameter that has a decorator. TypeScript restricts parameter decorators from being combined with the 'readonly' modifier. The fix involves removing the 'readonly' modifier from decorated parameters or restructuring your code to avoid this combination.
Quick fix
Fixes Parameter decorator cannot use 'readonly' modifier
// WRONG - readonly with parameter decorator
class UserService {
constructor(
@Inject(HttpService) readonly http: HttpService, // Error
@Inject(ConfigService) readonly config: ConfigService // Error
) {}
}
// CORRECT - remove readonly from decorated parameters
class UserService {
constructor(
@Inject(HttpService) http: HttpService, // Fixed
@Inject(ConfigService) config: ConfigService // Fixed
) {}
}- Remove the 'readonly' modifier from decorated parameters
- Use property assignment instead of constructor parameters
- Use private/protected instead of readonly for dependency injection
- Refactor to use property decorators instead of parameter decorators
// WRONG - readonly with parameter decoratorclass UserService {constructor(@Inject(HttpService) readonly http: HttpService, // Error@Inject(ConfigService) readonly config: ConfigService // Error) {}}// CORRECT - remove readonly from decorated parametersclass UserService {constructor(@Inject(HttpService) http: HttpService, // Fixed@Inject(ConfigService) config: ConfigService // Fixed) {}}Parameter decorator cannot use 'readonly' modifier
What this error means
The "Parameter decorator cannot use 'readonly' modifier" error appears when you attempt to combine a parameter decorator with the 'readonly' modifier in TypeScript. This is a language restriction that prevents certain combinations of modifiers and decorators on method or constructor parameters. Parameter decorators are used to annotate or modify parameters in methods and constructors, often for dependency injection frameworks, validation, or metadata attachment. The 'readonly' modifier makes a property or parameter immutable after initialization. TypeScript disallows this combination because: 1. **Semantic conflict**: Parameter decorators often imply runtime modification or inspection of the parameter, while 'readonly' suggests immutability 2. **Implementation complexity**: Supporting both features together would create complex edge cases in the TypeScript compiler 3. **Framework compatibility**: Many dependency injection frameworks that use parameter decorators expect to be able to modify or inject values into parameters This error commonly occurs in: - **Dependency injection frameworks** like Angular, NestJS, or InversifyJS - **Validation libraries** that use parameter decorators for input validation - **ORM frameworks** that decorate constructor parameters for entity instantiation - **Custom decorators** for logging, caching, or other cross-cutting concerns
Typical symptoms
- Compilation fails with 'Parameter decorator cannot use readonly modifier' error message
- Error occurs when using '@decorator readonly param: Type' syntax
- IDE shows red underlines beneath the 'readonly' keyword when used with parameter decorators
- Code works without the 'readonly' modifier but fails when adding it to decorated parameters
- Error appears in constructor parameters with both decorators and 'readonly' modifier
- Build process fails in frameworks like Angular, NestJS, or TypeORM when using this pattern
Common causes
- Using '@Inject() readonly service: Service' in constructor parameters
- Applying validation decorators like '@IsEmail() readonly email: string' in method parameters
- Combining ORM decorators with 'readonly' in entity constructors
- Using custom decorators for logging or caching on 'readonly' parameters
- Following patterns from other languages that allow this combination (like C#)
- Attempting to enforce immutability on dependency-injected parameters
- Copying code examples that work in JavaScript but not TypeScript due to this restriction
How to fix "Parameter decorator cannot use 'readonly' modifier"
The simplest fix is to remove the 'readonly' modifier from parameters that have decorators:
// WRONG - readonly with parameter decorator
class UserService {
constructor(
@Inject(HttpService) readonly http: HttpService, // Error
@Inject(ConfigService) readonly config: ConfigService // Error
) {}
}
// CORRECT - remove readonly from decorated parameters
class UserService {
constructor(
@Inject(HttpService) http: HttpService, // Fixed
@Inject(ConfigService) config: ConfigService // Fixed
) {}
}For method parameters:
// WRONG
class AuthController {
login(
@Body() readonly credentials: LoginDto, // Error
@Headers('Authorization') readonly authHeader: string // Error
) {
// ...
}
}
// CORRECT
class AuthController {
login(
@Body() credentials: LoginDto, // Fixed
@Headers('Authorization') authHeader: string // Fixed
) {
// ...
}
}If you need readonly properties with dependency injection, assign them in the constructor body:
// WRONG - readonly in decorated constructor parameters
class UserService {
constructor(
@Inject(HttpService) readonly http: HttpService, // Error
@Inject(Logger) readonly logger: Logger // Error
) {}
}
// CORRECT - assign readonly properties in constructor body
class UserService {
readonly http: HttpService;
readonly logger: Logger;
constructor(
@Inject(HttpService) http: HttpService,
@Inject(Logger) logger: Logger
) {
this.http = http;
this.logger = logger;
}
}This pattern is common in Angular and NestJS services:
// Angular/NestJS service pattern
@Injectable()
export class UserService {
readonly http: HttpService;
readonly config: ConfigService;
constructor(
http: HttpService,
config: ConfigService
) {
this.http = http;
this.config = config;
}
}If you need access control but not immutability, use private or protected modifiers:
// WRONG - readonly with decorator
class OrderService {
constructor(
@Inject(PaymentService) readonly payment: PaymentService, // Error
@Inject(EmailService) readonly email: EmailService // Error
) {}
}
// CORRECT - use private/protected
class OrderService {
constructor(
@Inject(PaymentService) private payment: PaymentService, // Fixed
@Inject(EmailService) protected email: EmailService // Fixed
) {}
processOrder() {
// Can still use this.payment and this.email
this.payment.charge();
this.email.sendReceipt();
}
}Note: 'private' and 'protected' don't prevent reassignment, they only control visibility:
class Example {
constructor(
@Inject(Service) private service: Service
) {}
// Can still reassign (unless you add additional checks)
changeService(newService: Service) {
this.service = newService; // Allowed with private, not with readonly
}
}For validation or metadata use cases, consider using property decorators:
// WRONG - parameter decorator with readonly
class UserDto {
create(
@IsEmail() readonly email: string, // Error
@MinLength(6) readonly password: string // Error
) {}
}
// CORRECT - use property decorators on a class
class UserDto {
@IsEmail()
email: string;
@MinLength(6)
password: string;
constructor(email: string, password: string) {
this.email = email;
this.password = password;
}
}
// Usage
const user = new UserDto('test@example.com', 'password123');
validate(user); // Validation framework checks property decoratorsFor class-validator or similar libraries:
import { IsEmail, MinLength } from 'class-validator';
class CreateUserDto {
@IsEmail()
email: string;
@MinLength(6)
password: string;
// Optional: make properties readonly at class level
constructor(email: string, password: string) {
this.email = email;
this.password = password;
Object.freeze(this); // Makes the entire instance immutable
}
}If you need compile-time immutability guarantees, use TypeScript's type system:
// Define a readonly interface
interface ReadonlyConfig {
readonly apiUrl: string;
readonly timeout: number;
readonly retries: number;
}
class ApiClient {
constructor(
@Inject(ConfigService) config: ConfigService // No readonly needed
) {
// Create readonly object from config
const readonlyConfig: ReadonlyConfig = {
apiUrl: config.get('API_URL'),
timeout: config.get('TIMEOUT'),
retries: config.get('RETRIES'),
};
// Use readonlyConfig which is immutable at compile time
}
}Using mapped types for readonly properties:
type Readonly<T> = { readonly [P in keyof T]: T[P] };
interface Config {
apiUrl: string;
timeout: number;
}
class Service {
private readonly config: Readonly<Config>;
constructor(@Inject(ConfigService) configService: ConfigService) {
this.config = {
apiUrl: configService.apiUrl,
timeout: configService.timeout,
};
}
// this.config is readonly at compile time
}Ensure you're using a compatible TypeScript version and configuration:
// tsconfig.json - ensure proper settings
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true, // Required for legacy decorators
"emitDecoratorMetadata": true, // Often needed with DI frameworks
// For TypeScript 5.0+ with stage 3 decorators:
// "experimentalDecorators": false, // Not needed for stage 3
// "useDefineForClassFields": true,
"strict": true
}
}TypeScript version compatibility:
- TypeScript 4.x and earlier: Uses legacy decorators with experimentalDecorators: true
- TypeScript 5.0+: Supports stage 3 decorators (no flag needed) but may have different restrictions
Check your framework's requirements:
- Angular: Requires experimentalDecorators: true (as of Angular 16)
- NestJS: Works with both legacy and stage 3 decorators
- TypeORM: Primarily uses legacy decorators
- class-validator: Works with both, but check version compatibility
If the error persists, try updating TypeScript and related packages:
# Update TypeScript
npm install typescript@latest
# Update framework packages
npm install @angular/core@latest # or @nestjs/common, typeorm, etc.Advanced notes
### Understanding the Technical Restriction
The restriction against combining 'readonly' with parameter decorators stems from how TypeScript transforms decorators and how parameters are represented in JavaScript.
Parameter decorators in JavaScript output:
When TypeScript compiles parameter decorators, they become function calls that receive:
1. The class prototype or constructor
2. The method name
3. The parameter index
The 'readonly' modifier affects how the parameter is represented in the emitted JavaScript, potentially conflicting with the decorator's ability to inspect or modify the parameter.
Example compilation:
// TypeScript source
class Example {
method(@Decorator() param: string) {}
}
// JavaScript output (simplified)
__decorate([
__param(0, Decorator)
], Example.prototype, "method", null);The 'readonly' modifier doesn't exist in JavaScript, so TypeScript must track it differently, creating potential conflicts with decorator transformations.
### Framework-Specific Workarounds
Angular/NestJS Dependency Injection:
// Instead of readonly in constructor
@Injectable()
export class UserService {
private readonly userRepo: UserRepository;
constructor(userRepo: UserRepository) {
this.userRepo = userRepo;
}
}
// Angular's inject() function (alternative approach)
@Injectable()
export class UserService {
readonly userRepo = inject(UserRepository);
}Validation Libraries (class-validator):
// Use property decorators instead of parameter decorators
class CreateUserDto {
@IsEmail()
@IsNotEmpty()
readonly email: string;
@MinLength(6)
readonly password: string;
constructor(email: string, password: string) {
this.email = email;
this.password = password;
}
}TypeORM Entities:
// Entity with readonly properties
@Entity()
export class User {
@PrimaryGeneratedColumn()
readonly id: number;
@Column()
readonly email: string;
// Constructor for dependency injection (if needed)
constructor(email: string) {
this.email = email;
}
}### Stage 3 Decorators (TypeScript 5.0+)
TypeScript 5.0 introduced ECMAScript stage 3 decorators with a different API. The restriction against 'readonly' with parameter decorators still applies, but the error message and behavior might differ.
Stage 3 parameter decorator signature:
function LogParameter(target: undefined, context: DecoratorContext) {
// context.kind === 'parameter'
// context.name === parameter name or index
}Migration considerations:
- Stage 3 decorators are not backward compatible with legacy decorators
- Many frameworks are gradually adding stage 3 support
- Check your framework's documentation before upgrading TypeScript
### Alternative Patterns
Factory functions for immutable instances:
function createUserService(http: HttpService, config: ConfigService) {
return Object.freeze({
http, // References are readonly
config,
getUser: (id: string) => http.get(`/users/${id}`)
});
}
// Usage
const userService = createUserService(httpService, configService);
// userService is immutableBuilder pattern with finalize method:
class ServiceBuilder {
private http?: HttpService;
private config?: ConfigService;
withHttp(http: HttpService) {
this.http = http;
return this;
}
withConfig(config: ConfigService) {
this.config = config;
return this;
}
build(): ReadonlyService {
if (!this.http || !this.config) {
throw new Error('Missing dependencies');
}
return Object.freeze(new ReadonlyService(this.http, this.config));
}
}
class ReadonlyService {
constructor(
public readonly http: HttpService,
public readonly config: ConfigService
) {}
}### Performance Considerations
Removing 'readonly' from parameters doesn't significantly affect runtime performance since 'readonly' is a compile-time check. However, alternative patterns like factory functions or builder patterns may have minor overhead.
The primary consideration should be code clarity and maintainability rather than micro-optimizations.
### Testing Implications
When testing classes that previously used 'readonly' parameters:
1. Mock dependencies can still be injected normally
2. The absence of 'readonly' doesn't affect testability
3. Consider adding runtime immutability checks if needed for tests
// Test example
describe('UserService', () => {
let mockHttp: jest.Mocked<HttpService>;
let service: UserService;
beforeEach(() => {
mockHttp = { get: jest.fn() } as any;
service = new UserService(mockHttp, mockConfig);
});
test('should not allow http reassignment', () => {
// Even without 'readonly', you can add runtime check
expect(() => {
(service as any).http = {} as HttpService;
}).toThrow(); // Or check that it doesn't actually reassign
});
});Frequently asked questions
- What does "Parameter decorator cannot use 'readonly' modifier" mean?
- The "Parameter decorator cannot use 'readonly' modifier" error appears when you attempt to combine a parameter decorator with the 'readonly' modifier in TypeScript. This is a language restriction that prevents certain combinations of modifiers and decorators on method or constructor parameters. Parameter decorators are used to annotate or modify parameters in methods and constructors, often for dependency injection frameworks, validation, or metadata attachment.
- How do I fix "Parameter decorator cannot use 'readonly' modifier"?
- This TypeScript error occurs when you try to use the 'readonly' modifier on a parameter that has a decorator. TypeScript restricts parameter decorators from being combined with the 'readonly' modifier. The fix involves removing the 'readonly' modifier from decorated parameters or restructuring your code to avoid this combination.
- What causes "Parameter decorator cannot use 'readonly' modifier"?
- Using '@Inject() readonly service: Service' in constructor parameters Applying validation decorators like '@IsEmail() readonly email: string' in method parameters Combining ORM decorators with 'readonly' in entity constructors
- Is the "Parameter decorator cannot use 'readonly' modifier" error serious?
- It is a medium-severity error — more of a blocker than a risk. The steps above resolve it.
Official resources & further reading
- 📚TypeScript Official Documentation
- 📖TypeScript Handbook - Decorators(Official Docs)
- 🐙
- 🐙
- 📖TypeScript 5.0 Release Notes - Stage 3 Decorators(Official Docs)