DEV Community

Cover image for Simple Value Types in Typescript
Jordan Quagliatini
Jordan Quagliatini

Posted on • Originally published at blog.jquagliatini.fr

Simple Value Types in Typescript

Value Types, or Value Objects, are core components of OOP design inheriting principles from Domain Driven Design, like entities and agregates. More often than not, entities have an identity: a unique domain identifier. They usually have a id field. Examples includes Users, Invoices, etc. Their content only doesn't differenciate enought between two entities.

On the other hand, Value Types, don't have a specific identity, but are identified by their content. It's not who they are, but what's their content. Like and Address, or an Email Address. They can introduce more semantic to your code, increase the safety, maintainability and testability. All this comes at the expanse of some degree of complexity. Especially in hydration and serialization scenarios.

For a lot of Value Types, they encapsulate a primitive value, wrapped and protected by domain specifications. Some are specific to a domain, and other cover more general use cases. In my opinion, introducing more complexity for domain Value Types is alright, but it's not for the generic ones. It's especially frustrating to have complex Value Type, when no behaviour is attached to theme.

How would you impement a simple, yet generic Value Type in Typescript?


Let's dive into an example, from a previous article, where we created a Window Iterable.

class AtLeastOneWindowIterable<T> implements Iterable<T[] | undefined> {
  protected constructor(
    private readonly values: readonly T[],
    private readonly size: number
  ) {}

  static of<U>(values: readonly U[]) {
    return {
      by: (size: number) => new AtLeastOneWindowIterable(
        values,
        size,
      ),
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

What if we increased the security by introducing a Value Type that only allow positive integers for the window size.

Let's start with the type:

type StrictlyPositiveInt = number & { __name: 'StrictlyPositiveInt' };
Enter fullscreen mode Exit fullscreen mode

Adding the & { __name: string } is common practice to create "Branded Types".
It prevents TS from keeping the structural equivalence between a number and StrictlyPostiveInt. If that the case, we can't simply assign a number. The following won't work, and that's exactly our intention:

const a: StrictlyPositiveInt = 3;
  //  〰️ Type 'number' is not assignable to type 'StrictlyPositiveInt'.
Enter fullscreen mode Exit fullscreen mode

We need a constructor:

function StrictlyPositiveInt(v: any): StrictlyPositiveInt {
  const n = Number(v);

  if (!Number.isFinite(n)) throw new Error(`${v} is not a number`);
  if (Math.round(n) !== n) throw new Error(`${n} is not an integer`);
  if (n <= 0) throw new Error(`${n} must be positive`);

  return n as StrictlyPositiveInt;
}
Enter fullscreen mode Exit fullscreen mode

If you ever used zod, that's equivalent to

import { z } from 'zod';

const StrictlyPositiveInt = z.number().int().min(1).brand('StrictlyPositiveInt');
type StrictlyPostiveIntT = z.infer<typeof StrictlyPositiveInt>;
Enter fullscreen mode Exit fullscreen mode

I encourage you to read their page on branded types.

Using it, becomes quite easy:

class AtLeastOneWindowIterable<T> implements Iterable<T[] | undefined> {
  protected constructor(
    private readonly values: readonly T[],
    private readonly size: StrictlyPositiveInt
  ) {}

  static of<U>(values: readonly U[]) {
    return {
      by: (size: number) => new this(
        values,
        StrictlyPositiveInt(size),
      ),
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice here, that the Iterable encapsulates the usage of the Value Type. In my opinion there are few reasons to expose internal rules to the outside world. From experience, having entities responsible for creating their Value Types is quite maintainable, even for more complex scenarios.

The only added complexity here comes from the exceptions. How you handle it will depend on your use cases. But usually, I just don't and prefer to let it bubble up.

Now, let's assume for a minute, that we need to send this StrictlyPositiveInt over the wire. No fuss or worries. We simply stringify it.

JSON.stringify(StrinctlyPositiveInt(32_000))
Enter fullscreen mode Exit fullscreen mode

Since, our Value Type, is only some type decoration, and runtime validation, there is no complexity. And since, our Iterable takes a number as input, the hydration is also painless. Win - Win!


Branded Types, Value Types, Value Objects. There are a ton of names for a simple logic encapsulation. Do you use it? How do you name it? Let me know.

Top comments (1)

Collapse
 
jquagliatini profile image
Jordan Quagliatini

For reference, this article from the Microsoft Blog on DDD might come useful:

learn.microsoft.com/en-us/dotnet/a...