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
hello
still 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