Create Value Objects
Encapsulate a string
// here BlogTitle will encapsulate a string value
// note that there is no need to let the complier know that its a string
export class BlogTitle extends ValueObject {}
const title = BlogTitle.from<string>('This is a blog about amazing value objects');
// here BlogTitle will encapsulate a string value
// note that there is no need to let the complier know that its a string
export class BlogTitle extends ValueObject {}
const title = BlogTitle.from<string>('This is a blog about amazing value objects');
Encapsulate a primitive
// here BlogLikeCount will encapsulate a number value
export class BlogLikeCount extends ValueObject<number> {}
const likes = BlogLikeCount.from<number>(10);
// here BlogLikeCount will encapsulate a number value
export class BlogLikeCount extends ValueObject<number> {}
const likes = BlogLikeCount.from<number>(10);
Add custom validation
export class BlogUrl extends ValueObject {
validate() {
super.validate();
if (!this.value.includes('/')) {
// NOTE: this is just an example and not an ideal way to check for URLs
throw new ValueObjectIsNotAnUrlException('Incorrect URL format');
}
}
}
const blogUrl = new BlogUrl('/');
export class BlogUrl extends ValueObject {
validate() {
super.validate();
if (!this.value.includes('/')) {
// NOTE: this is just an example and not an ideal way to check for URLs
throw new ValueObjectIsNotAnUrlException('Incorrect URL format');
}
}
}
const blogUrl = new BlogUrl('/');
Use Factory Methods when possible
By default it is possible to use new
keyword to create a new object, however, it is always a good idea to keep the constructor private and use the factory method to create objects
ValueObject.from()
The ValueObject class exposes a static factory method from<K>(value: ValueObjectType): K
that can be used to instantiate a new value object instance.
class MyValueObjectWithNumber extends ValueObject<number> {}
const hundred = MyValueObjectWithNumber.from<number>(100);
class MyValueObjectWithNumber extends ValueObject<number> {}
const hundred = MyValueObjectWithNumber.from<number>(100);
ValueObject.fromObject()
The ValueObject class exposes another static factory method fromObject<K>(data: unknown): K
that can be used to instantiate a new value object instance.
Now this is a special method that looks for a property named value
inside the data
object. If one exists, it will try to use that to create a value object.
class MySimpleValueObject extends ValueObject {}
const myVal = MySimpleValueObject.fromObject({ value: 'This is my value' });
// this works fine
console.log(myVal.value);
class MySimpleValueObject extends ValueObject {}
const myVal = MySimpleValueObject.fromObject({ value: 'This is my value' });
// this works fine
console.log(myVal.value);
However, if the data can not be converted to a ValueObject then an ObjectCanNotBeConvertedToValueObject
is thrown
class MySimpleValueObject extends ValueObject {}
// throws ObjectCanNotBeConvertedToValueObject
expect(() => MySimpleValueObject.fromObject({ invalid: true })).throws(ObjectCanNotBeConvertedToValueObject);
class MySimpleValueObject extends ValueObject {}
// throws ObjectCanNotBeConvertedToValueObject
expect(() => MySimpleValueObject.fromObject({ invalid: true })).throws(ObjectCanNotBeConvertedToValueObject);
👺 USE WITH EXTRA CAUTION!
The fromObject()
method can result in an inconsistent value object.
It is not yet smart enough to determine the value type and hence can result in a type mismatch
class MySimpleValueObject extends ValueObject<string> {}
// NOTE here we are passing a number but the value object expects a string
const myValue = MySimpleValueObject.fromObject({ value: 1000 }); //‼️⁉️
// myVal is successfully created
// The following test fails
expect(myValue.value).toEqual('1000'); // ❌ 👺
class MySimpleValueObject extends ValueObject<string> {}
// NOTE here we are passing a number but the value object expects a string
const myValue = MySimpleValueObject.fromObject({ value: 1000 }); //‼️⁉️
// myVal is successfully created
// The following test fails
expect(myValue.value).toEqual('1000'); // ❌ 👺
Complex Value Object
ValueObjects can not only hold simple value but also complex and nested value objects.
class MySimpleValueObject extends ValueObject {}
interface NestedValueObject extends CustomObject {
nested: MySimpleValueObject;
}
interface DeeplyNestedValueObject extends CustomObject {
deep: NestedValueObject;
}
interface ComplexValue extends CustomObject {
simpleString: string;
simpleNumber: number;
simpleBoolean: boolean;
simpleObject: { name: string };
simpleVO: MySimpleValueObject;
nestedVO: NestedValueObject;
deeplyNestedVO: DeeplyNestedValueObject;
}
class MyComplexValue extends ValueObject<ComplexValue> {}
const myValue = MyComplexValue.from<ComplexValue>({
simpleString: 'Hello World!',
simpleNumber: 100,
simpleBoolean: true,
simpleObject: { name: 'Bruce Wayne' },
simpleVO: MySimpleValueObject.from<string>('Hello World!'),
nestedVO: { nested: MySimpleValueObject.from<string>('Nested Value') },
deeplyNestedVO: { deep: { nested: MySimpleValueObject.from<string>('Deeply Nested Value') } },
});
class MySimpleValueObject extends ValueObject {}
interface NestedValueObject extends CustomObject {
nested: MySimpleValueObject;
}
interface DeeplyNestedValueObject extends CustomObject {
deep: NestedValueObject;
}
interface ComplexValue extends CustomObject {
simpleString: string;
simpleNumber: number;
simpleBoolean: boolean;
simpleObject: { name: string };
simpleVO: MySimpleValueObject;
nestedVO: NestedValueObject;
deeplyNestedVO: DeeplyNestedValueObject;
}
class MyComplexValue extends ValueObject<ComplexValue> {}
const myValue = MyComplexValue.from<ComplexValue>({
simpleString: 'Hello World!',
simpleNumber: 100,
simpleBoolean: true,
simpleObject: { name: 'Bruce Wayne' },
simpleVO: MySimpleValueObject.from<string>('Hello World!'),
nestedVO: { nested: MySimpleValueObject.from<string>('Nested Value') },
deeplyNestedVO: { deep: { nested: MySimpleValueObject.from<string>('Deeply Nested Value') } },
});