Branded types are a slightly hacky way to uniquely identify a property of some type or to ensure at compile time that a given value meets some additional type criterion.
Branded types should probably not be used too much since their usage is not really a use case intended by the language maintainers yet. That being said, there are some use cases where they may be practical.
- String values meeting specific formats
e.g.: email address, UUID, non-empty (#blank?
) - Specific properties of a given type
e.g.: non-empty for collections, not-only-whitespace for strings, non-zero for numbers
Since branded types aren't really an intentional and supported feature, they don't really play well with the boundaries of the type system.
If you find yourself kind of re-imlpementing a type hierarchy, make sure everything you implement has a real-world purpose or you may be unnecessarily going down an academic rabbit hole of type correctness, as is possible with any sufficiently strict type system.
First, define the brand or property you want to ensure at compile time as an
interface
with a single read-only property with a unique-ish name that is a
unique symbol...
interface UUID extends String {
readonly UUID: unique symbol;
}
- Using an
interface
instead of atype
can reveal when you have another thing defined with the same name and keeps the hover/completion docs succinct. - Using a
readonly
unique
symbol makes it so that no other type you could import will satisfy this type.
function isUUID(candidate: string): candidate is UUID {
return true; // Use a UUID regular expression to do this correctly...
}
Since the brand has a readonly
unique
symbol as a property, this can
effectively make this type guard the only way to validate that a given value
meets the type criteria. Yes, you can as any
and do whatever you want but then
why use Typescript in the first place..?
function handleID(id: string | UUID) {
if (isUUID(id)) {
// Show something special for an actual UUID.
} else {
// Fallback to handling a generic string.
}
}