In my previous blog post Branded Types I have shown a useful technique for representing time durations as "branded types" which removes the confusion between seconds and milliseconds.
1 | // value is in seconds |
Let me show an other example: representing user phone numbers using branded types. This solves a common problem in end-to-end tests where the system has to verify the user to avoid scammers:
1 | // are these numbers the same? |
In our Mercari testing, we use type 2 and type 3 phone numbers to represent real and "dev-only" phone numbers. We want to distinguish these phone numbers from regular strings. We want to statically "catch" the problem of passing a string where a phone number is required
1 | // custom command cy.enterPhoneNumber(phone: string) |
Branded phone types
Let's represent the 10-digit phone numbers using branded types.
1 | /** |
I like using ambient .d.ts files so that the common types are available everywhere in the testing repo. Right now we have the common "base" type PhoneNumber that is different from a string, and it is further "subdivided" into TestPhoneNumber and a RealPhoneNumber. Different utils and objects can specify the phone number types needed.
1 | // regular tests can use dev test numbers |
Great, but if all we know is that we have a string, how do we ensure that it is a valid phone number? Well, we can check:
- test phone numbers are 10-digit strings that start with "000"
- real phone numbers are 10-digit strings that do not start with "000" or "555"
- formatted phone numbers follow their own pattern
We can write TypeScript assertion functions for easy type casting:
1 | /** |
The base phone number in our system is either a test phone number or a real phone number:
1 | /** |
Here is how we can use it: imagine we have a string with a phone number.
1 | const phoneNumber = '0001234567' |
If we check the variable when it is declared, we see what TypeScript sees: a string

We could be explicit and declare the variable type
1 | const phoneNumber = '0001234567' as TestPhoneNumber |
But we could assert the type by checking the string, then TS "knows" the current type, and the runtime validates it
1 | const phoneNumber = '0001234567' |
Look at the use of the variable phoneNumber after the assertTestPhoneNumber call:

Beautiful. We can receive data from our backend and immediately cast it by validating
1 | cy.request(...) |
There is no more confusion, the branded types let us statically check the variables. Yet every test and real phone number is also a string, so we can pass and use them, just like any string
1 | cy.request(...) |