Skip to main content
interlace
Plugin: modularityRules

ddd-value-object-immutability

Report value-object classes whose instance fields can be reassigned after construction.

Report a value-object class whose instance fields can be reassigned. A value object is defined by its value; if it can change after construction, two holders of "the same" value drift apart.

This rule is part of eslint-plugin-modularity.

Quick Summary

AspectDetails
SeverityError (code quality)
Auto-Fix❌ No
CategoryModularity
Suggestions✅ Add readonly, freeze in the constructor

Rule Details

A class is a value object when its name ends with one of valueObjectPatterns (default Value, VO, ValueObject). The rule reports such a class when it declares instance state without readonly:

  • a class field (amount: number;);
  • a constructor parameter property (constructor(public amount: number)), which declares the same field.

Static fields are class state, not the value of an instance, and are not checked.

It does not report a value object that:

  • declares only readonly fields and readonly parameter properties;
  • calls Object.freeze(this) in its constructor (while checkObjectFreeze is true). Freezing a nested value, Object.freeze(this.items), leaves the instance's own fields writable and does not count.

The check is shallow: a readonly items: string[] field passes, though the array itself stays mutable.

Configuration

OptionTypeDefaultDescription
valueObjectPatternsstring[]['Value', 'VO', 'ValueObject']Class-name suffixes that mark a value object
requireReadonlybooleantrueReport fields without readonly. false turns the check off
checkObjectFreezebooleantrueAccept Object.freeze(this) in the constructor

Examples

❌ Incorrect

class MoneyValueObject {
  amount: number;
  constructor(amount: number, public currency: string) {
    this.amount = amount;
  }
}

✅ Correct

class MoneyValueObject {
  readonly amount: number;
  constructor(amount: number, private readonly currency: string) {
    this.amount = amount;
  }
}

class PriceVO {
  amount = 0;
  constructor() {
    Object.freeze(this);
  }
}

Configuration Examples

// eslint.config.mjs
export default [
  {
    rules: {
      'modularity/ddd-value-object-immutability': ['error', { valueObjectPatterns: ['Money', 'VO'] }],
    },
  },
];

Use this rule

npm install -D oxlint-plugin-modularity
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-modularity"],  "rules": {    "modularity/ddd-value-object-immutability": "error"  }}
vite.config.ts
import { defineConfig } from 'vite-plus';export default defineConfig({  lint: {    jsPlugins: ['oxlint-plugin-modularity'],    rules: {      'modularity/ddd-value-object-immutability': 'error',    },  },});

oxlint-plugin-modularity re-exports eslint-plugin-modularity/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.

Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.