Branded Types
This principal works perfectly together with a parser / validator. A perfect
example would be zod
which uses Branded
Types to make objects as validated.
Lets assume we want to create a Password
type:
type Password = string;
And we have a function which creates a user:
function createUser(username: string, password: Password): void {
// creation of a user
// ...
}
TypeScript would allow us to call the function as follows:
createUser('username', 'some-password');
This is because Password
is just a type alias for string
. However, we rather
would like to pass Passwords to the createUser
function which have been
validated and meet our password requirements. To achieve this, we can use
Branded Types.
To brand our Password
type, we simply intersect it with an object:
// '__brand' can be named to your liking.
// Some also name it '__type'.
type Password = string & {__brand: 'Password'};
Now we have a type which we normally wouldn't be able to create since we cant
have a string
which at the same time is an object
.
Note that this type doesn't resolve to never
which is important. We, without a
doubt, can't create never
types. However we can tell TypeScript that we know
more about a specific variable type than itself.
Let's create another function which takes a password and returns whether its valid or not:
function validatePassword(password: string): password is Password {
if (password.length < 8) return false;
// ... some other checks
return true;
}
We now can use this function to tell TypeScript: "Hey TypeScript. If this
function returns true
treat the provided parameter password
as a Password
rather than a string
".
These type of functions are also called
Type Guards
in TypeScript.
Now that we have your validate function in place we can call createUser
:
const password = '12345678';
if (validatePassword(password)) {
createUser('username', password);
}
If you prefer not to nest your code or rather use guards we can create a
function called assertValidPassword
:
function assertValidPassword(password: string): asserts password is Password {
if (password.length < 8) {
throw new Error('Password needs to be at least 8 characters long.');
}
}
This function must throw an Error
and doesn't return a boolean
like the
validatePassword
function. The assertValidPassword
function can now be used
once and afterwards TypeScript treats the provided parameter as a Password
:
const password = '1234';
assertValidPassword(password);
createUser('username', password);