Last tended April 6, 2024.
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 Email
isEmail = (email: string
email: string):email: string
email is type Email = string & {
[brand]: "email";
}
Email=> { return email: string
email.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.includes("@gmail.com") };
function function sendEmail(email: Email): void
sendEmail(email: Email
email: 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
```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.log("sent.");
}
function function trySendEmail(email?: string): void
trySendEmail(email: string
email = "user@gmail.com"){
// @ts-expect-error
function sendEmail(email: Email): void
sendEmail(email: string
email); // string is not assignable to paramter of type `Email`
if( const isEmail: (email: string) => email is Email
isEmail(email: string
email) ) {
function sendEmail(email: Email): void
sendEmail(email: Email
email)
} 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
```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.log("not an email")
}
}
function trySendEmail(email?: string): void
trySendEmail()
In this example I make a “branded” Email
type and use the isEmail
function to narrow validated strings to Email
s which is useful in a case such as trySendEmail()
.
Branding can also be done with a reusable generic type:
declare const const brand: typeof brand
brand: 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 brand
brand]: 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: UserID
id: type UserID = number & {
[brand]: "User ID";
}
UserID; username: string
username: 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 = string
Password = string;
type type ValidPassword = string & {
[brand]: "Valid";
}
ValidPassword = type Brand<T, TBrand extends string> = T & {
[brand]: TBrand;
}
Brand<type Password = string
Password, "Valid">;
function function isValidPassword(password: Password): password is ValidPassword
isValidPassword(password: string
password: type Password = string
Password): password: string
password is type ValidPassword = string & {
[brand]: "Valid";
}
ValidPassword {
if( password: string
password.String.length: number
Returns the length of a String object.length >= 8) return true;
return false;
}
let let pass: string
pass: type Password = string
Password = "12345678";
if(function isValidPassword(password: Password): password is ValidPassword
isValidPassword(let pass: string
pass)){
let pass: ValidPassword
pass
// 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 ValidPassword
assertPasswordIsValid(password: string
password: type Password = string
Password): asserts password: string
password is type ValidPassword = string & {
[brand]: "Valid";
}
ValidPassword {
if(password: string
password.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: string
pass: type Password = string
Password = "12345678";
function assertPasswordIsValid(password: Password): asserts password is ValidPassword
assertPasswordIsValid(let pass: string
pass);
let pass: ValidPassword
pass
// Hover over `pass` to see it's now typed as `ValidPassword` - no `if` statements needed!