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.
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
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('[email protected]', '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.### 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
});
});Function expression requires a return type
Function expression requires a return type
Value of type 'string | undefined' is not iterable
How to fix "Value is not iterable" in TypeScript
Type 'undefined' is not assignable to type 'string'
How to fix "Type undefined is not assignable to type string" in TypeScript
Type narrowing from typeof check produces 'never'
How to fix "Type narrowing produces never" in TypeScript
Type parameter 'T' has conflicting constraints
How to fix "Type parameter has conflicting constraints" in TypeScript