Value object - a Typescript implementation

I regained the desire, drive and enthusiasm about a month ago and threw myself into getting acquainted with Domain Driven Design and Typescript. It has been a very educational period where I not only got back the architectural thinking and mentality but also started to like Typescript.

Typescript - implementation of a value object

I'm not going into if you should use Typescript or not in this article. There are pros- and cons about using it which this article will not cover. This article will on the other hand go through some basic understanding of Typescript (and JavaScript) in order to understand my implementation of value object I'm using in my "research" of Domain-Driven Design.

Typescript basics

First we need to understand some basic about Typescript interface definitions and common data types in both Typescript and JavaScript.

We start with a simple Typescript example;

interface Square {
  with: number      // Required declaration
  color?: string    // Optional decalaration
}

function createSquare(config: Square) {
  const newSquare = {
    width: config.width,
    color: config.color ? config.color : 'default'
  }
}

In this simple example, we can see how we use required and optional properties for a square interface where the width property is required but not the color property.

Interfaces with optional properties are written similar to other interfaces, with each optional property denoted by a (?) at the end of the property name in the declaration.

These optional properties are popular when creating patterns like “option bags” where you pass an object to a function that only has a couple of properties filled in.

In TypeScript, interfaces can also describe indexable types i.e. the types which can be accessed via indexes. Indexable types have an index signature that describes the types we can use to index into the object, along with the corresponding return types when indexing. Let’s take an example:

interface ObjectDictionary {
  [index: string]: string
}

let myDict: ObjectDictionary = { name: 'Henrik' }
let myOtherDict: ObjectDictionary = { 'username': 'Henrik' }

let myStrValueDict: string = myObjDict.name
let myStrValueOtherDict: string = myObjDict['username']

Above, we have a ObjectDictionary interface that is an indexable interface with an index signature. This index signature states that an ObjectDictionary is indexed with a string, and will return a string.

The code above works since objects in JavaScript can be thought of as a dictionary. They can take arbitrary properties (keys), which map to corresponding values. This object - dictionary duality is also visible in JavaScript syntax:

const myObject = { name: 'Henrik' };
const keyValue = myObject['name']
const keyValue2 = myObjetc.name

We could also see that the naming of the property for the indexable interface did not matter, we named it "name" in the first occasion and "username" in the other.

To enforce naming of properties of the indexable interface, we can add them to the indexable interface;

interface ObjectDictionary {
  [index: string]: string
  username: string // Returns the same type as the index signature
}

let myObjDict: ObjectDictionary = { name: 'Henrik' }
//  Fails: Property 'username' is missing in type '{ name: string; }' 
//  but required in type 'ObjectDictionary'.

let myDict: ObjectDictionary = { username: 'Henrik' }
// Works

Here we added a known property called username to the indexable interface which should be of type string. It is valid since it returns the same type as the indexable type. The username property is a way for us to name the property in advance before using it. This also means we can not use other names of the indexable property.

It is important to understand that properties can be added and removed in runtime and in general, most objects have a constant known set of properties, and you model them as regular interfaces in Typescript.

We need to use an indexable type if we want to model objects which follow JavaScript rules such as;

  • can contain any property,
  • can be modified in runtime by adding and deleting properties.

Note that there are 2 common kinds of indices in JavaScript;

  • number - you can only index via a number (which means you use them like an array)
  • string - you can index via any string (you use them like a dictionary)

Typescript generics

A major part of software engineering is building components that not only have well-defined and consistent APIs, but are also reusable. Components that are capable of working on the data of today as well as the data of tomorrow will give you the most flexible capabilities for building up large software systems.

In languages like C# and Java, one of the main tools in the toolbox for creating reusable components is generics, that is, being able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.

To start off, let’s do the “hello world” of generics: the value function. The value function is a function that will return whatever is passed in. You can think of this as the echo command.

Without generics, we would either have to give the value function a specific type:

function value(arg: number): number {
  return arg;
}

Or, we could describe the value function using the any type:

function value(arg: any): any {
  return arg
}

A quick note about the any type in Typescript: In some situations, not all type information is available or its declaration would take an inappropriate amount of effort. These may occur for values from code that has been written without TypeScript or a 3rd party library. In these cases, we might want to opt-out of type checking. To do so, we label these values with the any type:

declare function getValue(key: string): any
// OK, return value of 'getValue' is not checked
const str: string = getValue("myString")

The any type is a powerful way to work with existing JavaScript, allowing you to gradually opt-in and opt-out of type checking during compilation.

Back to generics: While using any is certainly generic in that it will cause the function to accept any and all types for the type of arg, we actually are losing the information about what that type was when the function returns. If we passed in a number, the only information we have is that any type could be returned.

Instead, we need a way of capturing the type of the argument in such a way that we can also use it to denote what is being returned. Here, we will use a type variable, a special kind of variable that works on types rather than values.

function value<T>(arg: T): T {
  return arg
}

We’ve now added a type variable T to the value function. This T allows us to capture the type the user provides (e.g. number), so that we can use that information later. Here, we use T again as the return type. On inspection, we can now see the same type is used for the argument and the return type. This allows us to traffic that type information in one side of the function and out the other.

We say that this version of the value function is generic, as it works over a range of types. Unlike using any, it’s also just as precise (ie, it does not lose any information) as the first value function that used numbers for the argument and return type.

Once we’ve written the generic value function, we can call it in one of two ways. The first way is to pass all arguments, including the type argument, to the function:

let output = value<string>("myString")
//                 ^ = let output: string

Here we explicitly set the type variable T to be string as one of the arguments to the function call, denoted using the <> around the arguments rather than ().

The second way is also perhaps the most common. Here we use type argument inference — that is, we want the compiler to set the value of the type variable T for us automatically based on the type of the argument we pass in:

let output = value("myString")
//    ^ = let output: string

Notice that we did not have to explicitly pass the type in the angle brackets (<>); the compiler just looked at the value "myString", and set the type variable T to its type. While type argument inference can be a helpful tool to keep code shorter and more readable, you may need to explicitly pass in the type arguments as we did in the previous example when the compiler fails to infer the type, as may happen in more complex example.

Now it's time to head over to my implementation of the value object.

Implementation of a value object

My implementation of the value object I'm using in my journey to learn more about Domain-Driven Design took some time for me to create since I needed to understand the basics about Typescript.

The value object implementation:

interface ValueObjectProps {
  [index: string]: any
}

abstract class ValueObject<T extends ValueObjectProps> {
  public props: T
  private constructor(props: T) {
    this.props = {
      ...props,
    }
  }
}

As the name indicate I'm using it for objects and that means it can be viewed as a dictionary type. Remember objects in JavaScript can take arbitrary properties, which map to corresponding values

The class own properties (props) has been declared as a generic type T, and the properties (props) passed into the constructor has also been declared as generic type T.

The class itself has been declared as an abstract class and abstract classes are base classes from which other classes may be derived. They may not be instantiated directly. Unlike an interface, an abstract class may contain implementation details for its members.

The class explicit declare its own type via the generic variable T which extends the indexable type ValueObjectProps.

How do I used it?

interface ValueObjectProps {
  [index: string]: any
}

export abstract class ValueObject<T extends ValueObjectProps> {
  public props: T
  protected constructor(props: T) {
    this.props = {
      ...props,
    }
  }
}

interface UserNameProps {
  username: string
}

class UserName extends ValueObject<UserNameProps> {
  constructor(props: UserNameProps) {
    super(props)
  }
  
  get Value() {
    return this.props
  }
  
  get username() {
    return this.props.username
  }
}

let vo = new UserName({ username: 'Henrik' })
let value = vo.Value
let username = vo.username

console.log(vo)
//  UserName { props: { username: 'Henrik' } }
console.log(value)
// { username: 'Henrik' }
console.log(username)
// Henrik

First, we can see the UserName class inherits from the abstract class ValueObject, and we passed the UserName own interface (UserNameProps) to the type variable T in the ValueObject class. The type variable T in the ValueObject extends the indexable interface (ValueObjectProps), which consist of an indexable signature and returns type any. You can think about it that we have now named the allowed properties to the indexable interface ValueObjectProps. The allowed property name is now username for the indexable interface ValueObjectProps.

The parameters to the constructor of the UserName class accepts only the declaration of its own interface (UserNameProps) which also only accept a property named username with a return type of string.

When we instantiate a new UserName object, the actual value is stored in the ValueObject own variable props which can be accessed by getter or setter methods in the sub-class UserName instance. Now it is up to the developer to design getters and setters for the value object they create.

Note that I have declared the constructor of the value object as protected and thus it is only available for derived classes. I have also left out the equality comparison method implemented in the ValueObject class. In Domain-Driven Design value object are treated as equal by their value of the properties.

Another design issue I have not decided on is if the value objects own properties should be declared protected or public. If they are protected, the derived class must implement getters and setters for all of them, if they are declared public the developer can access them without getters or setters.

Here is why I like the indexable types and its power. A pure bonus:

// Defining reusable indexable type
interface States {
  [state: string]: boolean
}

let moodStates: States = {'isHungry': true, 'mustCookFood':false}
if (moodStates.isHungry) {
  console.log(moodStates.mustCookFood)
  // false
}

Summary

That's all for now, I'm not an expert of Typescript, I think it has some nice features, I like especially the ide of coding against an interface and using interfaces for dependency injection.

References:


Published: 2021-03-02
Author: Henrik Grönvall
Henrik Grönvall
Copyright © 2022 Henrik Grönvall Consulting AB