Type branding removes structural type compatibility

🌱 February 3, 2024.
Last tended April 6, 2024.
budding 🌿
3 minutes read ⏱

Chalk it up to growing up on C++, but sometimes I find it helpful to name a type and limit the usage based its name.

Consider an email address input. A user can type any string! I have no idea if they typed a valid email or not, all I know is it’s a string. Once I validate it with Zod I know more, but as far as TypeScript is concerned it’s still just a string. I could mistakenly assign something else to it, or accidentally use a different value when calling a function that requires a validated email. This is a problem in TypeScript because it uses structural types - data with the same shape is considered equivalent. This is in contrast to nominal types which are only compatible with objects that share the same type name.

Fortunately there’s a TypeScript technique called “Branded types” that makes it possible to reduce a structural type’s compatibility to the level of a nominal type. It is also a great way to encode information about a value in the type system.

/** `brand` is a globally unique value, so the only ways to create an "Email" type are 
* 1. "branding" a string using `as Email` 
* 2. copying an already branded `Email`
*/
declare const const brand: typeof brand
`brand` is a globally unique value, so the only ways to create an "Email" type are 1. "branding" a string using `as Email` 2. copying an already branded `Email`
brand
: unique symbol;
type
type Email = string & {
    [brand]: "email";
}
Email
= string & { [const brand: typeof brand
`brand` is a globally unique value, so the only ways to create an "Email" type are 1. "branding" a string using `as Email` 2. copying an already branded `Email`
brand
]: "email"}
const const isEmail: (email: string) => email is EmailisEmail = (email: stringemail: string):email: stringemail is
type Email = string & {
    [brand]: "email";
}
Email
=> { return email: stringemail.String.includes(searchString: string, position?: number): boolean
Returns true if searchString appears as a substring of the result of converting this object to a String, at one or more positions that are greater than or equal to position; otherwise, returns false.
@paramsearchString search string@paramposition If position is undefined, 0 is assumed, so as to search all of the String.
includes
("@gmail.com") };
function function sendEmail(email: Email): voidsendEmail(email: Emailemail:
type Email = string & {
    [brand]: "email";
}
Email
) {
var console: Console
The `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v24.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
@see[source](https://github.com/nodejs/node/blob/v24.x/lib/console.js)
console
.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args) for more information.
@sincev0.1.100
log
("sent.");
} function function trySendEmail(email?: string): voidtrySendEmail(email: stringemail = "user@gmail.com"){ // @ts-expect-error function sendEmail(email: Email): voidsendEmail(email: stringemail); // string is not assignable to paramter of type `Email` if( const isEmail: (email: string) => email is EmailisEmail(email: stringemail) ) { function sendEmail(email: Email): voidsendEmail(email: Emailemail) } else { var console: Console
The `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v24.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
@see[source](https://github.com/nodejs/node/blob/v24.x/lib/console.js)
console
.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args) for more information.
@sincev0.1.100
log
("not an email")
} } function trySendEmail(email?: string): voidtrySendEmail()

In this example I make a “branded” Email type and use the isEmail function to narrow validated strings to Emails which is useful in a case such as trySendEmail().

Branding can also be done with a reusable generic type:

declare const const brand: typeof brandbrand: unique symbol;
// The Brand generic can be used to augment any structural data type with nominal type safety
type 
type Brand<T, TBrand extends string> = T & {
    [brand]: TBrand;
}
Brand
<function (type parameter) T in type Brand<T, TBrand extends string>T, function (type parameter) TBrand in type Brand<T, TBrand extends string>TBrand extends string> = function (type parameter) T in type Brand<T, TBrand extends string>T & {[const brand: typeof brandbrand]: function (type parameter) TBrand in type Brand<T, TBrand extends string>TBrand}
type
type Email = string & {
    [brand]: "Email";
}
Email
=
type Brand<T, TBrand extends string> = T & {
    [brand]: TBrand;
}
Brand
<string, "Email"> // works the same as the `Email` type in the previous example
type
type Password = string & {
    [brand]: "Password";
}
Password
=
type Brand<T, TBrand extends string> = T & {
    [brand]: TBrand;
}
Brand
<string, "Password"> // Easy to reuse
type
type UserID = number & {
    [brand]: "User ID";
}
UserID
=
type Brand<T, TBrand extends string> = T & {
    [brand]: TBrand;
}
Brand
<number, "User ID"> // can be used with any primitive type
type
type DBUser = {
    id: UserID;
    username: string;
} & {
    [brand]: "Database User";
}
DBUser
=
type Brand<T, TBrand extends string> = T & {
    [brand]: TBrand;
}
Brand
<{id: UserIDid:
type UserID = number & {
    [brand]: "User ID";
}
UserID
; username: stringusername: string}, "Database User">; // and with object types

The Brand utility type makes it easy to encode information about a value in its type. I find this helpful as values move through an application. Branded types can represent:

  • Unique keys in a database
  • Hashed passwords
  • Validated emails
  • Verified emails
  • IDs for different schemas such as users or posts
  • A User object that came from a database record
  • and more!

Narrowing to a branded type using type predicates and assertion functions

Brands can also be used with type predicate functions to narrow a structural type to a branded type in something like a validation function. For example:

type type Password = stringPassword = string;
type 
type ValidPassword = string & {
    [brand]: "Valid";
}
ValidPassword
=
type Brand<T, TBrand extends string> = T & {
    [brand]: TBrand;
}
Brand
<type Password = stringPassword, "Valid">;
function function isValidPassword(password: Password): password is ValidPasswordisValidPassword(password: stringpassword: type Password = stringPassword): password: stringpassword is
type ValidPassword = string & {
    [brand]: "Valid";
}
ValidPassword
{
if( password: stringpassword.String.length: number
Returns the length of a String object.
length
>= 8) return true;
return false; } let let pass: stringpass: type Password = stringPassword = "12345678"; if(function isValidPassword(password: Password): password is ValidPasswordisValidPassword(let pass: stringpass)){ let pass: ValidPasswordpass // Hover `pass` above to see it's now typed as `ValidPassword` }

If you want to reduce the need for nested if statements or include guards, or prefer to treat a false case as an error/exception, consider using assertion functions instead:

function function assertPasswordIsValid(password: Password): asserts password is ValidPasswordassertPasswordIsValid(password: stringpassword: type Password = stringPassword): asserts password: stringpassword is 
type ValidPassword = string & {
    [brand]: "Valid";
}
ValidPassword
{
if(password: stringpassword.String.length: number
Returns the length of a String object.
length
< 8) throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
("Password too short")
} let let pass: stringpass: type Password = stringPassword = "12345678"; function assertPasswordIsValid(password: Password): asserts password is ValidPasswordassertPasswordIsValid(let pass: stringpass); let pass: ValidPasswordpass // Hover over `pass` to see it's now typed as `ValidPassword` - no `if` statements needed!