Published on

Modeling Value Objects in TypeScript

Authors
Attention!

You don't want to read my observations about "Value Object" as a concept?

Skip to the "meaty" part!

How to be deceived?

I remember the first time I read Blue Book and how easily I put all of the focus on the tactical patterns. It might be that the reason was because I finally "found my methods". I briefly described it in Where did my methods go?.

Sometimes I believe that dealing with the complexity of the systems gives us, technical people, the rush full of dopamine. We feel dizzy and dazed because we need to understand all of the abstractions, terms, inner workings, etc. Like sugar, this drug is really subtle to reveal.

Value Object

That was the first time I met a concept of Value Object. I completely misunderstood its meaning, invaluable perspective it brings. Please bear in mind I don't state I understand it and its meaning now 😉.

Let's take a "typical" (I don't like this word, but it fits nicely in the current context) definition, or rather traits description.

Again, this definition by description of its traits sounds very tactical. It speaks with technical jargon. Identity, immutability - they are really nice. Once you tasted immutability it's hard to forget (I am looking at you F#!).

How we could describe a "Value Object"?
  • it has no identity

  • it is immutable

Don't be deceived only by the great properties it has. One day, I suddenly started observing (I hope so at least 😛) its second, deeper nature.

What is the true nature of Value Object?

If we leave tactical patterns for a while and go to strategic ones, we land in the realm where the focus is on modeling using a proper language.

Now we are in the kingdom where The King of All Kings, The Context, is ruling with his strict but fair influence.

And it gives exactly what you can express with a Value Object - a meaning.

Suddenly, we can consciously materialize concepts and build the language using programming contructs. How cool is that?

What problem does "Value Object" really solve?

It flavors the code with conceptual meaning from the context it comes from.

I have a gut feeling that in order to utilize anything, we need to build a language construct (a model?) of it so that we can consciously operate on it (instead of randomly "oh look what just happened").

Use case: modeling an email concept

Someone can say: "Hey, but we can easily express domain concepts in TypeScript!".

No doubt we can. Question is, is it the right model we are using?

To the point. I mean, example.

Let's imagine the scenario that suddenly we need to add a functionality that would enable sending emails. Our user types an email address and then we need to process is accordingly. Email has its own specification, but we will simplify it, a bit.

Our rule we need to ensure is that the email address needs to have @ character.

The most straightforward approach we could imagine is the following:

export type Email = string
Attention!

One could ask why I didn't use the new TS feature called Template Literal Types to properly shape the type, including @ in between.

As modeling an email concept is kind of "special", because it's based on string, it would be quite easy to enforce proper structure by using aforementioned feature.

I decided to work with this simple example, because I believe most of the people know how to detect invalid email 😊

Of course, the capacity of a string is huge so our "model" (yes, it is a model) is very, very liberal.

import { Email } from "./email"
const trulyAnEmail: Email = "I am an email"

How we can protect our rule? Well, a function:

export const isValidEmail = (possiblyAnEmail: Email) => 
    !(possiblyAnEmail.indexOf("@") === -1)

The problem (of course!) is that those two things are separated, even though living in the same file, i.e. email.ts. Nothing stops us from not calling isValidEmail function.

How to tackle it? We could of course put an object wrapper around it, like that:

type Email = { value: string }

But still nothing prevents us from creating a new instance without checking its validity. If we call something an "email", it should be it!

"Functional-oriented" Encapsulation

Cool kids know that this is an example of encapsulation issue. So currently our main problem we want to solve is: how to properly encapsulate "email" concept?

Using type aliases feels very "functional", because it's quite natural to create those types as keeping only data and then declare functions that operate on those types (maybe it is only natural to me? 😛)

In languages like F#, we could easily declare the type within a module, but make its "constructor" private, meaning that outside the module we can only use it, e.g. in function signature, but not create a value with such type.

Let's take an example:

// Email.fs
module Email =
    type Email = private Email of email: string

    let create (possiblyAnEmail: string) =
        if possiblyAnEmail.Contains("@")
            then Email possiblyAnEmail
            else failwith "Not an email :-("

//SomeOtherFile.fs
open Email // use the module `Email`

let anEmail: Email = Email "I fooled you!" // bzzzzt, compile time error!

Unfortunately, as far as I know TypeScript (or JavaScript?) modules do not support accesibility modifiers in the way we want now:

  • declare a type and export it (make it read-only without being able to instantiate it)
  • you can freely instantiate that within that module (or file)

Could we try to use OO way to encapsulate this concept?

Exaggerated "Object-oriented" Encapsulation

How one does OO encapsulation? Of course, using a class.

Let's try to model it iteratively.

// initial-oo-email.ts
export class Email {
  private value: string;
  readonly isValid: boolean;
  readonly errorMessage?: string;
  constructor(value: string) {
    this.value = value;
    this.isValid = this.isValidEmail(value);

    if (!this.isValid) {
      this.errorMessage = `Oops, "${value}" is not a true email :-(`;
    }
  }

  private isValidEmail(possiblyAnEmail: string) {
    return !(possiblyAnEmail.indexOf('@') === -1);
  }
}


No-brainer usage:

// initial-oo-index.ts
import { Email } from './initial-oo-email';

const invalidEmail = new Email('I am an email');
const validEmail = new Email('john@johnny.com');

console.log(invalidEmail); // Email {value: "I am an email", isValid: false}
console.log(validEmail); // Email {value: "john@johnny.com", isValid: true}

const youCantInitializeMe: Email = {
  value: 'I am not an email',
  isValid: true,
}; // bzzzzt, value is my private field!

Yes, I made this example a bit exaggerated, because Email might be misleading - we can create an instance and there check if it is actually valid by checking its isValid property.

But...Have you ever seen an email with "a label" sticking from it with text informing whether it is valid or not?

Nope, I assume you look at the email and you know if it is either valid or invalid. Thus we name them distinctively.

In other words, when we create an instance of the email, we want to be sure that we are talking about valid email, else its invalid.

"Object-oriented" Encapsulation

Let's again use the main building block of OO - class, but now a bit differently.

// email-goto.ts
const isValidEmail = (possiblyAnEmail: string) => {
  return !(possiblyAnEmail.indexOf('@') === -1);
};

export class Email {
  private value: string;
  private constructor(value: string) {
    this.value = value;
  }

  static from(value: string): Email {
    if (isValidEmail(value)) return new Email(value);

    throw new Error(`Oops, "${value}" is not a true email :-(`);
  }
}

Does it solve our problem mentioned at the beginning of "Functional-oriented" Encapsulation??

// oo-goto-index.ts
import { Email } from './email-goto';

try {
  const invalidEmail = Email.from('I am an email');
} catch (e) {
  console.log(e.message); // Oops, "I am an email" is not a true email :-(
}

const validEmail = Email.from('john@johnny.com');
console.log(validEmail); // Email {value: "john@johnny.com"}

That's really nice. We have exactly what we wanted to get. We are able to get either invalid or valid email. Sweet.

Let's talk briefly about the thrown Error. As it is common practice for other languages (like C#, Java) to use exceptions as control flow mechanisms, I think we can utilize the power of TS types and provide more controlled solution.

Global error handling might (and probably will) catch it flawlessly, but it feels "GOTO"-ish, when we suddenly jump across multiple layers of our code. What we could do?

Listen to the language

I intentionally concluded that "we are able to get either invalid or valid email". Paying attention to this subtle terminology, let's model those cases accordingly.

Attention!

Some parts of the code might get a bit overwhelming.

From now on such code blocks will appear as explandable sections marked with 💻 emoji.

Refactor time!

💻 Show refactored code! 💻
// email.ts
const isValidEmail = (possiblyAnEmail: string) => {
  return !(possiblyAnEmail.indexOf('@') === -1)
}

// new!
type ValidEmail = {
  type: 'VALID_EMAIL'
  context: {
    email: Email
  }
}

// new!
const validEmailAs = (email: Email): ValidEmail => ({
  type: 'VALID_EMAIL',
  context: { email },
})

// new!
type InvalidEmail = {
  type: 'INVALID_EMAIL'
  context: {
    errorMessage: string
  }
}

//new!
const invalidEmailFrom = (value: string): InvalidEmail => ({
  type: 'INVALID_EMAIL',
  context: { errorMessage: `Oops, "${value}" is not a true email :-(` },
})

export class Email {
  private value: string
  private constructor(value: string) {
    this.value = value
  }

  static from(value: string): InvalidEmail | ValidEmail { // <=== new return type!
    if (isValidEmail(value)) return validEmailAs(new Email(value))

    return invalidEmailFrom(value)
  }
}

We are trying to speak with the language of our small domain and we provided new language constructs inside of our code.

Now the caller has the power (and the responsibility that comes with this power 😉):

// oo-index.ts
import { Email } from './email'

const invalidEmail = Email.from('I am an email')
console.log(invalidEmail) // { type: "INVALID_EMAIL", context: { errorMessage: "Oops, "I am an email" is not a true email :-(" }}

const validEmail = Email.from('john@johnny.com')
console.log(validEmail) // { type: "VALID_EMAIL", context: { email: "john@johnny.com" } }

const youCantInitializeMe: Email = {
  value: 'I am not an email',
}; // bzzzzt, value is my private field!

It looks fantastic. Now we could embed only valid email type in our complex sending function.

// oo-index.ts
function sendingEmailTo(validEmail: ValidEmail) { // bzzzzt, no `ValidEmail` type available!
    // complex code
}

We put a constraint in the sendingEmailTo function telling the reader that it can be used only with ValidEmail. That's pretty neat, isn't it?

This constraint is checked in compile-time! Oh, btw. our code doesn't compile.

We didn't export ValidEmail nor InvalidEmail types purposefully so that no one is able to "cheat" and create it outside of the module.

Still that's pretty easy if you know how to extract the type from the union type.

// oo-index.ts
type ValidEmail = Extract<ReturnType<typeof Email["from"]>, { type: "VALID_EMAIL" }>

On the other hand, exposing those types should be rather safe (in this particular context).

"Functional-oriented" Encapsulation - revisited!

Ok, fine. We are able to model Value Objects as OO-style classes, but is "Functional way" lost? Should we abandon it saying it's impossible?!

Fortunately, there are geniuses in this world. Thanks to Michael Arnaldi I learned that there's hope - non-exported property approach!

Turns out it is well-established concept called Opaque or Branded types. Let's jump into usage to understand what might be the cause of those names.

Email "domain" modeling made "functional"

In order to achieve what was suggested by Michael, we need to utilize Symbol primitive. It has very unique property - it is guaranteed it will be unique. You can find more details here.

Let's write some code!

Attention!

I didn't put the code that will be similar to what we saw in OO-style, namely:

  • ValidEmail type
  • validEmailAs function
  • InvalidEmail type
  • invalidEmailFrom function
  • isValidEmail function

Imagine they are available somewhere in the branded-email.ts file 😉

// let's assume we have `isValidEmail`, `validEmailAs` and `invalidEmailFrom` functions, `ValidEmail` and `InvalidEmail` types available

// branded-email.ts
const _emailBrand: unique symbol = Symbol("EMAIL") // NOTE WE ARE NOT EXPORTING IT!
export type Email = {
    readonly [_emailBrand]: typeof _emailBrand,
    readonly value: string
}

export const from = (possiblyAnEmail: string): ValidEmail | InvalidEmail => 
    isValidEmail(string)?
        validEmailAs({ 
            [_emailBrand]: _emailBrand,
            value: possiblyAnEmail 
        })
        : invalidEmailFrom(value)

And its usage:

//non-module-index.ts
import { from, Email } from "./branded-email"

const invalidEmailFromBranded = from('I am an email')
console.log(invalidEmailFromBranded) // { type: "INVALID_EMAIL", context: { errorMessage: "Oops, "I am an email" is not a true email :-(" }}

const validEmailFromBranded = from('john@johnny.com')
console.log(validEmailFromBranded) // { type: "VALID_EMAIL", context: { email: "john@johnny.com" } }

const youCantInitializeMe: Email = {
    value: "I am not an email"
} // bzzzzt, you need to have a symbol to initialize me!

Well, at least it works. One might say that this is huge disadvantage of Functional Programming - functions floating here, floating there. Our lovely from function is a great example.

It gives no context, and as we know, The Context is the king of kings.

Can we do better? Just use module "notation"!

// module-index.ts
import * as Email from './branded-email'

const invalidEmailFromBranded = Email.from('I am an email') // { type: "INVALID_EMAIL", context: { errorMessage: "Oops, "I am an email" is not a true email :-(" }}
console.log(invalidEmailFromBranded)

const validEmailFromBranded = Email.from('john@johnny.com')
console.log(validEmailFromBranded) // { type: "VALID_EMAIL", context: { email: "john@johnny.com" } }

const youCantInitializeMe: Email.Email = {
    value: "I am not an email"
} // bzzzzt, you need to have a symbol to initialize me!

Modeling with encapsulation: "Functional-oriented" vs "Object-oriented"?

It might be a matter of preference which of those approaches is "better" (whatever it means in this context).

One interesting observation came from this little experiment - OO-style Email.from looks really similar to FP-style module-based Email.from.

Conclusion 🔍

Use modules when working with functions so that they are not floating around deattached from their context!

This might be counter-intuitive, especially when you need to grab one, tiny function, which is a valid point. Soon, it might call friends and there would be an army of "little" functions with no context attached.

I would say it doesn't matter that much which "style" you actually use. As long as you model a given concept precisely enough in the context in which it "lives", then you are already the winner.

On the other hand, I feel that functional languages are a bit further in modeling, because of their Sum and Product types, that enable you to OR-ing and AND-ing (check Domain Modeling Made Functional for further details, because it's explains it with a great metaphor!).

Conclusion 🔍

When modeling, pay attention to the language used - words, terms, concepts - and materialize them using types.

For example, in F# (and TS too!) I really feel encouraged to properly express various terms and concepts like InvalidEmail and ValidEmail.

As a last step in our exercise, knowing all we've seen, here's the question:

Question 🤔

How could we model a forbidden email which must be valid one, but its domain is not in the "allowed" domains collection?

All code examples

If you want to play with the code examples we went through during our little journey, you can find them here.

Main file is index.ts and then it loads sub-main modules. Each refers to a separate step in our experiment. You can comment/uncomment each of the import to see the effects.