February 14, 2023

Introduce your own data types

We deal with data all the time, especially primitive data like strings, numbers, and booleans. Most of the time they represent our values well—we store user names in strings and their ages in numbers, for example.

But sometimes a value is more than just a string or a number; take a phone number for example. A full phone number consists of three parts: a country code, an area code, and a local phone number.

If you have phone numbers as strings, then there would be no clear way to access any of its parts directly; you have to parse it each time you want to do that. And the more phone number formats you want to support, the more complicated your parsing code would be.

Another thing about phone numbers is that they can be represented in many different formats—for example, some contain parenthesis, others don’t. If you don’t know what format you’re receiving, then it’s your job to parse and display it in the desired format. If this code is not extracted some where, then you’ll end up repeating it each time you deal with a phone number.

The best way to handle all of these design issues is to introduce your own phone number type—by type I mean a PhoneNumber class.

Code example

I’ll use the same phone number example. I can think of two approaches to implement the PhoneNumber class. The first approach is to create it as a wrapper of the original string, and add methods to operate on that string—such as getAreaCode and getLocalPhoneNumber methods. The other approach (the more sophisticated approach) is to parse the phone number value in the constructor and set its areaCode and localNumber as properties on the class.

Here’s how I would write it if I want it to work as a wrapper:

class PhoneNumber {
  #rawPhoneNumber

  constructor(phoneNumber) {
    this.#rawPhoneNumber = phoneNumber
    if (!this.#isValid()) {
      throw new Error('invalid phone number')
    }
  }

  #isValid() {
    // return if this.#rawPhoneNumber is valid
  }

  get areaCode() {
    // extract and return the area code from this.#rawPhoneNumber string
  }

  get localNumber() {
    // extract and return the local number from this.#rawPhoneNumber string
  }

  get countryCode() {
    // extract and return the country code from this.#rawPhoneNumber string
  }

  toString() {
    // return how the phone number should be displayed as a string
  }
}

Creating your own data type doesn’t mean you lose the ability to treat it as a string when you want. Adding toString() handles the case of displaying the object in a string context. For example, this would work as if the phone number object is a string:

const phoneNumber = new PhoneNumber(phoneNumberString)
console.log(`the phone number is: ${phoneNumber}`)

Not only that, but also with toString() you can display it using any format you want.

Second approach

The other approach (the one I prefer), would look like this:

class PhoneNumber {
  #rawPhoneNumber
  #countryCode
  #areaCode
  #localNumber

  constructor(phoneNumber) {
    this.#rawPhoneNumber = phoneNumber
    if (!this.#isValid()) {
      throw new Error('invalid phone number')
    }
    this.#countryCode = this.#extractCountryCode()
    this.#areaCode = this.#extractAreaCode()
    this.#localNumber = this.#extractLocalNumber()
  }

  #isValid() {
    // return if this.#rawPhoneNumber is valid
  }

  #extractCountryCode() {
    // extract and return country code from this.#rawPhoneNumber
  }

  #extractAreaCode() {}
  #extractLocalNumber() {}

  get areaCode() {
    return this.#areaCode
  }

  set areaCode(value) {
    // Check if the provided value is invalid
    if (!value) {
      throw new Error('invalid area code')
    }
    this.#areaCode = value
  }

  get localNumber() {}
  set localNumber(value) {}

  get countryCode() {}
  set countryCode(value) {}

  toString() {
    // return how the phone number should be displayed as a string
  }
}

One advantage of the second approach is that you can now have setters for its parts—this would make it mutable, which some might see as a bad code smell, but that’s for another post. If you don’t like setters, though, you can just remove them.

Another advantage is that we’ve cached the phone number parts, which is good for performance especially if parsing the data is an expensive process—which is not for this simple example.

As with all good code design updates, the code becomes more testable; now you have a clear way to test phone numbers.

Stay up-to-date on the latest projects and articles from me