Value Objects – DDD with TypeScript
khalilstemmler.com
khalilstemmler.com
How about this code instead:
export interface User {
readonly name: string;
}
export namespace User {
export function validate(user: User) {
// In "strict" TS, `name` is required at compile time to not be null or undefined.
if (name.length < 2 || name.length > 100) {
throw new Error('User must be greater than 2 chars and less than 100.');
}
}
}
Unlike the OOP solution, there's no guarantee that any given `User` is valid unless you check it with `User.validate`, but there should be minimal places if not one single place where users are actually validated, such as where they are saved to the database or backend.This is why we use private constructors. The Value Object is a place to encapsulate that domain logic.
The private constructor removes that very possibility and ensures the only way to create a User is to use the static factory method, which validates the User.
I admit, when I first saw the layers of encapsulation, it was a bit off to me. But that's a small price to pay for model integrity.
Why do you need to restrict this possibility? How many places in your code create new users, and how many places in your code need to validate users? If the answer is “many,” you have serious design problems that should not be solved by introducing more classes but by restricting how many different areas of code do the same job.
Also, the private constructors do absolutely nothing. The static factory methods in this blog post could be written the exact same way as a constructor, and they would work exactly the same. The only reason the author uses a static factory method is because that's what he was taught to do in Java.
It's just OOP for OOP's sake in a language with tons of features designed for avoiding OOP.
Consider an application where you can edit and add users. Consider that you can also add users to groups and remove users from groups. Consider that you might also be able to add policies to users. These are a few different areas in code where you might need to create an instance of a User.
This is exactly what we're doing; we're limiting the amount of places where valid users can get created. The User domain object and it's invariants are validated right here at the model.
If there was an invariant saying that users can't have more than 5 policies on them, a user.addPolicy(policy) method would exist right on the user model/aggregate root, and the aggregate root would be responsible for determining if any invariants will be unsatisfied after the operation.
> Also, the private constructors do absolutely nothing. The static factory methods in this blog post could be written the exact same way as a constructor, and they would work exactly the same. The only reason the author uses a static factory method is because that's what he was taught to do in Java.
The reason why I use a static factory method is because it allows you to protect the aggregate invariants (domain logic that deems the model to be correct). The language has nothing to do with it.
If I was to create Coordinates, I need to ensure that the Latitude and Longitude fulfill the domain requirements, and I want to be able to know if I was able to create that object or not.
This approach usually means using a Result class to handle errors (https://enterprisecraftsmanship.com/2015/03/20/functional-c-...).
I'll be writing another article on that topic.
This is a different way of programming, for sure. DDD takes a bit of time to wrap your head around. But when projects are sufficiently complex, it pays off to know that there's no possibility for an invalid aggregate to be floating around at runtime.
> It's just OOP for OOP's sake in a language with tons of features designed for avoiding OOP.
Could not be more incorrect.
If you're interested in learning more about DDD, check out "DDD Distilled" for a quick primer on it and why the chosen patterns are used. There's a method to the madness.
Think of it as like building a Domain-Specific language that any of the 4 developers in your company that need to write code will be able to use without breaking any rules of the domain. If it's just you writing code on a project that is relatively non-complex, DDD is probably not the optimal choice.
I do believe it's better to be strict but allow a loophole just in case future use requires it.
For example in my codebase, only valid objects can be created via User.create but we do create invalid ones for testing since it's not necessary to have full objects there.
There are also ways to opt-in and out of following rules like this, even from a TypeScript context- you just have to be more aggressive about it.
For example, you could access private methods by doing:
User['create']() for example. Sometimes this is necessary for testing.
Might be worth checking that library out for anyone who knows about DDD or wants to abstract their mapping code.
I get that typescript is only a compile time check and doesn't guarantee type correctness at runtime, but if I feel like there might be larger problems here to solve first if my function takes in type string and the first thing I have to do is make sure it's not null or undefined. If you legitimately expect either a string or null, there are proven ways to deal with this (Maybe/Optional).
I'm working on a DDD framework for Typescript (https://www.npmjs.com/package/@node-ts/ddd) and am keen to add something like this
I guess the closest thing in typescript would be a type alias, but apparently it doesn't do all that much?
type Path = string & { _brand: never };
const Path = (s: string) => validatePath(s) as Path;
const p1: Path = Path('/foo/bar.js');
const p2: Path = '/foo/bar.js';
Here, p2 has an error because a string is not assignable to a Path.But yes, I'm wary of too many base classes and libraries designed to help you manage your model. Don't use someone else's ValueObject type, instead, work out what makes sense for your model.