Refine existing types
When the type you have at hand is not precise enough, refine it with a predicate.
The types are too wide
In this program, user names come from two sources:
- a new user enters a name,
- a third party service returns a list of names.
For the purpose of this example two things are true:
- a valid username is a non blank string with no whitespace at beginning or end,
- the third party service is sloppy with their output.
Unfiltered names from the third party are available via the getDirtyData function from ./dirty-data. Its type signature is () => Array<string>.
The original program blithely processes invalid names.
This is not desirable. The hello function must act on valid input in order for its message to look good (no extra spaces) and make sense.
Clearly we need some validation.
Argument validation
We can validate inside the called function.
There are two problems with this:
- Calls to
hellostill fail (though no longer silently), and by the time the error is thrown the program might not be in a position to re-show the input dialog. - The type system does not help us. The same validation needed to be added to every function taking this kind of argument but the compiler didn’t notice that we didn’t add it to
goodbye.
Input validation
We could instead validate the user input.
The type system doesn’t help us here either. We didn’t add validation to getDirtyData and only discovered the oversight on running the program.
The type is too wide
string is not the right type for our user names. To be used safely it requires that all inputs and arguments be validated. This is possible but suffers from a lack of type system support. Forget to validate an argument? You’ll find out at runtime. Update isValidUsername because you got it wrong the first time? You might discover at runtime that some functions were using the equally broken bobsMagicValidator instead.
Narrow the type
A refined type is essentially a type paired with a predicate. They are tied together by a “smart constructor” which allows only valid instances to be constructed.
Here is Username, a refinement of string. The only way to construct a user is with the mkUser function which throws on invalid input. This creates the compile time guarantee that all Username values are valid.
This refinement has zero runtime overhead. There is no wrapping going on. All the original type’s properties can be accessed on values of the refined type.
With this refined type no argument validation inside hello and goodbye is necessary. Inside those functions the argument is guaranteed to be a Username and Username values are guaranteed valid by construction.
The type system will enforce that callers provide a Username argument. This enforcement will propagate up the call stack to the point where a Username is constructed. Much like an exception, but at compile time!
If we forget to validate a string to a Username in some input (like getDirtyData), the compiler will tell us.
Where next?
- Either or Validation for validated construction of a refined type
- Safe strings for safe interpolation
- General tag type for easy refined type creation