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.