export class Email {
private constructor(readonly value: string) {}
static create(raw: string): Email {
const v = raw.trim().toLowerCase();
if (!/^[^@\s]+@[^@\s]+$/.test(v)) throw new InvalidEmailError(raw);
return new Email(v);
}
}
An always-valid object checks its rules (invariants) when it is created and in every method that changes it. So if you hold an instance, it is valid. Code that uses it doesn't need to check again.
How:
- Make the constructor
private. Thennew Email("x")from outside doesn't compile. - Offer a static factory such as
Email.create(raw). It cleans and validates the input, then calls the constructor. It can throw, or return aResulttype. - Keep fields
readonlyor private. Change state only through methods that check rules, likeorder.addLine()refusing a 51st line.
Loading from the database: the stored data was valid when it was saved. Rules may have changed since, though. Two common options:
- A separate factory,
Email.fromPersistence(value), that skips the checks. Name it clearly so only the repository uses it. - Or run
createagain, and treat a failure as a data problem to log and fix.
Remember: private constructor is a TypeScript check only. Plain JavaScript or as any can still get around it.
- An always-valid object checks its invariants on creation and on every change.
- A private constructor plus a static factory forces every caller through validation.
- State changes go through methods that check the rules, not through setters.
- Loading from storage can use a clearly named factory that skips the checks.
private constructoris only enforced by TypeScript, not at runtime.
Read more