Éste es el error que más rápido se detecta y más cuesta entender, porque el síntoma no señala a la causa. Y es el que la propia librería tuvo publicado durante dos versiones.
El código
export class Price extends NumberValueObject {
private readonly max = 9_999_999.99;
override addValidators(): void {
super.addValidators();
this.validatorRules.add(new MaxValidator(this, this.max)); // ← undefined
}
}
Revienta. En cada construcción, sin excepción.
La causa: el orden real de ejecución
Cuando escribes new Price(49.99), JavaScript hace esto:
1. Se ejecuta el constructor de la clase BASE
2. ...y dentro, la base llama a this.addValidators()
3. ...que resuelve a TU override, porque el despacho es dinámico
4. ...y ahí lees this.max, que todavía no existe ← 💥
5. Termina la base
6. AHORA se inicializan los campos de tu subclase: this.max = 9999999.99
7. Se ejecuta el cuerpo de tu constructor
El paso 3 es la clave y es contraintuitivo: un método sobrescrito se puede llamar antes de que tu clase esté construida. El despacho dinámico no espera a que tú estés listo.
Los inicializadores de campo (private readonly max = ...) se compilan a asignaciones que TypeScript coloca después de la llamada a super(). Así que en el paso 4, this.max es literalmente undefined.
Las tres soluciones
// 1. Constante de módulo — la más simple
const MAX_PRICE = 9_999_999.99;
export class Price extends NumberValueObject {
override addValidators(): void {
super.addValidators();
this.validatorRules.add(new MaxValidator(this, MAX_PRICE));
}
}
// 2. static readonly — si el valor pertenece conceptualmente a la clase
export class Price extends NumberValueObject {
static readonly MAX = 9_999_999.99;
override addValidators(): void {
super.addValidators();
this.validatorRules.add(new MaxValidator(this, Price.MAX));
}
}
// 3. El validador se lo sabe — si la regla es suya, no tuya
export class MaxPriceValidator extends AbstractRuleValidator<Price> {
private static readonly MAX = 9_999_999.99;
// ...
}
Por qué el compilador no ayuda
Porque el tipo de this.max es number, y lo es. TypeScript no modela «este campo aún no está inicializado en este punto del ciclo de vida» — sólo modela «este campo existe y es de este tipo».
Es la misma familia de problemas que las clases base que llaman a métodos virtuales en el constructor en C# o Java, donde el compilador tampoco avisa. La diferencia es que aquí lo hace una librería, no tu código, así que ni siquiera ves la llamada.
Se puede automatizar
npx ddd validate tiene una regla para esto: no-subclass-state-in-add-validators. Es un predicado sobre el AST, corre en CI y no necesita ningún modelo. Los detalles están en la documentación del CLI.
La regla que llevarte
Si un método tuyo puede ser llamado por la clase base, trátalo como si corriera antes de que exista tu objeto. Sólo puede usar: sus argumentos, constantes de módulo, miembros estáticos y estado que la base ya haya inicializado. Nada más.
