
Value Objects – DDD with TypeScript - stemmlerjs
https://khalilstemmler.com/articles/typescript-value-object/
======
bfrydl
This seems like overengineering and a needless application of OOP. The author
presents ideas like service classes or static factory methods as if they are
self-evident, but I don't understand what would make someone write TypeScript
code this way besides too much Java experience. The final solution has the
value of the “name” field nested multiple objects deep!

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.

~~~
stemmlerjs
The point of DDD is to encapsulate your domain models to keep the invariants
satisfied. How can you restrict the very possibility of creating invalid
instances of a User this way? There's nothing stopping me from using the "new"
keyword and making an invalid user with a name 400 characters long.

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.

~~~
bfrydl
> How can you restrict the very possibility of creating invalid instances of a
> User this way? There's nothing stopping me from using the "new" keyword and
> making an invalid user with a name 400 characters long.

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.

~~~
stemmlerjs
> 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.

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-...](https://enterprisecraftsmanship.com/2015/03/20/functional-
c-handling-failures-input-errors/)).

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.

------
dfee
I’ve just gotten io-ts configured to do mapping in / out of Firestore (mapping
from a Query/DocumentSnapshot on the ingress and to a change set on egress).

Might be worth checking that library out for anyone who knows about DDD or
wants to abstract their mapping code.

[https://github.com/gcanti/io-ts](https://github.com/gcanti/io-ts)

------
jasonhansel
The distinction between "value objects" and "entities" reminds me of:
[https://en.m.wikipedia.org/wiki/Haecceity](https://en.m.wikipedia.org/wiki/Haecceity)

------
SmooL
As an aside, is no one else bothered by the ubiquitous null/undefined checks?
Almost every function implementation here is started with checking that the
args are actually what is expected.

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).

------
redact207
I like this approach. Just knowing what value objects are and where to use
them in your code is a good improvement all around.

I'm working on a DDD framework for Typescript
([https://www.npmjs.com/package/@node-
ts/ddd](https://www.npmjs.com/package/@node-ts/ddd)) and am keen to add
something like this

~~~
stemmlerjs
Wow, nice work! Starred it to dig around your repo my next lunch break.

------
skybrian
In Go this would make sense, but having to wrap a string in an object just so
that you can declare a new type for it seems like too high a price to pay.

I guess the closest thing in typescript would be a type alias, but apparently
it doesn't do all that much?

~~~
spankalee
You can do much better than just a type alias in TypeScript, and get
specializations of primitives that type check and compile away.

    
    
        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.

~~~
skybrian
That's a neat trick! I went searching for articles about it and came up empty,
though I did find other interesting uses of the "never" type.

------
alanpetrel
Ugh, applying DDD to TypeScript/client-side code just seems like over-
engineering. DDD makes server-side code horrendously complex and difficult to
maintain, I can't see it being any better here.

~~~
enb
The whole point of DDD is to reduce complexity by modeling the domain in such
a way that you can change it. There are many times when DDD is a wrong choice
for your given problem. Perhaps the code you've seen wasn't a good candidate
for DDD.

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.

~~~
stemmlerjs
Love this response. Very well said.

