5 ms·
Value Objects – DDD with TypeScript
- SmooL 8y agoAs 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).
- jasonhansel 8y agoThe distinction between "value objects" and "entities" reminds me of: https://en.m.wikipedia.org/wiki/Haecceity https://en.m.wikipedia.org/wiki/Haecceity
- alanpetrel 8y agoUgh, 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 8y agoThe 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 8y agoLove this response. Very well said.
- dfee 8y agoI’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
- redact207 8y agoI 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 8y agoWow, nice work! Starred it to dig around your repo my next lunch break.
- skybrian 8y agoIn 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 8y agoYou 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 8y agoThat'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.
- bfrydl 8y agoThis 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 8y agoThe 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.
- true_religion 8y agoWell you could use the Python approach of "we are all adults" and allow people to do the wrong thing if they are explicitly asking to do it. 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.