Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/reference/value-object.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ abstract class ValueObject<T extends Primitive = Primitive>
constructor(value: T | null | undefined)
```

## Runtime immutability

The stored `value` cannot be reassigned, deleted, or redefined, including from JavaScript. Writes in strict mode throw a `TypeError`; non-strict assignments leave the value unchanged.

Only the stored value is protected. Subclasses can initialize additional fields after `super()`, and are responsible for protecting those fields if needed. Subclasses must pass their final primitive value to `super()` instead of redeclaring or assigning `value` afterward.

## NullObject creation

Automatic NullObject creation is enabled by default. A `null` or `undefined` constructor value returns a compatible null object instead of storing the nullish value.
Expand Down
32 changes: 32 additions & 0 deletions scripts/verify-distribution.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,38 @@ const main = async () => {
const esm = await import(esmUrl);
verifyExports(esm, 'ESM');

for (const runtime of [cjs, esm]) {
const email = new runtime.Email('user@example.com');
assert.equal(Reflect.set(email, 'value', 'invalid'), false);
email.value = 'invalid'; // Non-strict JavaScript must leave the value intact.
assert.throws(() => {
'use strict';
email.value = 'invalid';
}, TypeError);
assert.equal(Reflect.deleteProperty(email, 'value'), false);
assert.throws(
() => Object.defineProperty(email, 'value', { value: 'invalid' }),
TypeError,
);
assert.equal(email.valueOf(), 'user@example.com');

class LabeledEmail extends runtime.Email {
label = 'work';
}
const labeled = new LabeledEmail('user@example.com');
assert.equal(labeled.label, 'work');
assert.equal(labeled.valueOf(), 'user@example.com');

class Status extends runtime.Enum {
getValues() {
return ['active', 'inactive'];
}
}
assert.equal(new Status('active').valueOf(), 'active');
assert.throws(() => new Status('invalid'), runtime.ValueNotInEnumError);
assert.ok(runtime.NullObject.isNullObject(new Status(undefined)));
}

for (const name of Object.keys(cjs)) {
assert.strictEqual(
esm[name],
Expand Down
2 changes: 1 addition & 1 deletion src/value-objects/Enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ValueObject } from './ValueObject';
export abstract class Enum<
T extends Primitive = Primitive,
> extends ValueObject<T> {
constructor(protected readonly value: T) {
constructor(value: T) {
super(value);

if (NullObject.isNullObject(this)) {
Expand Down
7 changes: 6 additions & 1 deletion src/value-objects/ValueObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@

return NullObject.new(new.target);
}
this.value = value;
Object.defineProperty(this, 'value', {
value,
writable: false,
configurable: false,

Check warning on line 29 in src/value-objects/ValueObject.ts

View workflow job for this annotation

GitHub Actions / Test and coverage

Expected "configurable" to come before "writable"
enumerable: true,
});
}

private isNullish(value: T | Nullish): value is null | undefined {
Expand Down