In this guide, we will explore the concept of branded types in TypeScript and learn how to create and work with them using the Brand module.
Branded types are TypeScript types with an added type tag that helps prevent accidental usage of a value in the wrong context.
They allow us to create distinct types based on an existing underlying type, enabling type safety and better code organization.
The Problem with TypeScript’s Structural Typing
TypeScript’s type system is structurally typed, meaning that two types are considered compatible if their members are compatible.
This can lead to situations where values of the same underlying type are used interchangeably, even when they represent different concepts or have different meanings.
Consider the following types:
Here, UserId and ProductId are structurally identical as they are both based on number.
TypeScript will treat these as interchangeable, potentially causing bugs if they are mixed up in your application.
Example (Unintended Type Compatibility)
1
type
typeUserId=number
UserId=number
2
3
type
typeProductId=number
ProductId=number
4
5
const
constgetUserById: (id:UserId) =>void
getUserById= (
id: number
id:
typeUserId=number
UserId) => {
6
// Logic to retrieve user
7
}
8
9
const
constgetProductById: (id:ProductId) =>void
getProductById= (
id: number
id:
typeProductId=number
ProductId) => {
10
// Logic to retrieve product
11
}
12
13
const
constid:number
id:
typeUserId=number
UserId=1
14
15
constgetProductById: (id:ProductId) =>void
getProductById(
constid:number
id) // No type error, but incorrect usage
In the example above, passing a UserId to getProductById does not produce a type error, even though it’s logically incorrect. This happens because both types are considered interchangeable.
How Branded Types Help
Branded types allow you to create distinct types from the same underlying type by adding a unique type tag, enforcing proper usage at compile-time.
Branding is accomplished by adding a symbolic identifier that distinguishes one type from another at the type level.
This method ensures that types remain distinct without altering their runtime characteristics.
Let’s start by introducing the BrandTypeId symbol:
1
const
constBrandTypeId:typeof BrandTypeId
BrandTypeId:uniquesymbol=
var Symbol:SymbolConstructor
Symbol.
SymbolConstructor.for(key: string): symbol
Returns a Symbol object from the global symbol registry matching the given key if found.
Otherwise, returns a new symbol with this key.
@param ― key key to search for.
for("effect/Brand")
2
3
type
typeProductId=number& {
readonly [BrandTypeId]: {
readonlyProductId:"ProductId";
};
}
ProductId=number& {
4
readonly [
constBrandTypeId:typeof BrandTypeId
BrandTypeId]: {
5
readonly
typeProductId: "ProductId"
ProductId:"ProductId"// unique identifier for ProductId
6
}
7
}
This approach assigns a unique identifier as a brand to the number type, effectively differentiating ProductId from other numerical types.
The use of a symbol ensures that the branding field does not conflict with any existing properties of the number type.
Attempting to use a UserId in place of a ProductId now results in an error:
Example (Enforcing Type Safety with Branded Types)
1
const
constBrandTypeId:typeof BrandTypeId
BrandTypeId:uniquesymbol=
var Symbol:SymbolConstructor
Symbol.
SymbolConstructor.for(key: string): symbol
Returns a Symbol object from the global symbol registry matching the given key if found.
Otherwise, returns a new symbol with this key.
@param ― key key to search for.
for("effect/Brand")
2
3
type
typeProductId=number& {
readonly [BrandTypeId]: {
readonlyProductId:"ProductId";
};
}
ProductId=number& {
4
readonly [
constBrandTypeId:typeof BrandTypeId
BrandTypeId]: {
5
readonly
typeProductId: "ProductId"
ProductId:"ProductId"
6
}
7
}
8
9
const
constgetProductById: (id:ProductId) =>void
getProductById= (
id: ProductId
id:
typeProductId=number& {
readonly [BrandTypeId]: {
readonlyProductId:"ProductId";
};
}
ProductId) => {
10
// Logic to retrieve product
11
}
12
13
type
typeUserId=number
UserId=number
14
15
const
constid:number
id:
typeUserId=number
UserId=1
16
17
// @ts-expect-error
18
constgetProductById: (id:ProductId) =>void
getProductById(
constid:number
id)
19
/*
20
Argument of type 'number' is not assignable to parameter of type 'ProductId'.
21
Type 'number' is not assignable to type '{ readonly [BrandTypeId]: { readonly ProductId: "ProductId"; }; }'.ts(2345)
22
*/
The error message clearly states that a number cannot be used in place of a ProductId.
TypeScript won’t let us pass an instance of number to the function accepting ProductId because it’s missing the brand field.
Let’s add branding to UserId as well:
Example (Branding UserId and ProductId)
1
const
constBrandTypeId:typeof BrandTypeId
BrandTypeId:uniquesymbol=
var Symbol:SymbolConstructor
Symbol.
SymbolConstructor.for(key: string): symbol
Returns a Symbol object from the global symbol registry matching the given key if found.
Otherwise, returns a new symbol with this key.
@param ― key key to search for.
for("effect/Brand")
2
3
type
typeProductId=number& {
readonly [BrandTypeId]: {
readonlyProductId:"ProductId";
};
}
ProductId=number& {
4
readonly [
constBrandTypeId:typeof BrandTypeId
BrandTypeId]: {
5
readonly
typeProductId: "ProductId"
ProductId:"ProductId"// unique identifier for ProductId
6
}
7
}
8
9
const
constgetProductById: (id:ProductId) =>void
getProductById= (
id: ProductId
id:
typeProductId=number& {
readonly [BrandTypeId]: {
readonlyProductId:"ProductId";
};
}
ProductId) => {
10
// Logic to retrieve product
11
}
12
13
type
typeUserId=number& {
readonly [BrandTypeId]: {
readonlyUserId:"UserId";
};
}
UserId=number& {
14
readonly [
constBrandTypeId:typeof BrandTypeId
BrandTypeId]: {
15
readonly
typeUserId: "UserId"
UserId:"UserId"// unique identifier for UserId
16
}
17
}
18
19
declareconst
constid:UserId
id:
typeUserId=number& {
readonly [BrandTypeId]: {
readonlyUserId:"UserId";
};
}
UserId
20
21
// @ts-expect-error
22
constgetProductById: (id:ProductId) =>void
getProductById(
constid:UserId
id)
23
/*
24
Argument of type 'UserId' is not assignable to parameter of type 'ProductId'.
25
Type 'UserId' is not assignable to type '{ readonly [BrandTypeId]: { readonly ProductId: "ProductId"; }; }'.
26
Types of property '[BrandTypeId]' are incompatible.
27
Property 'ProductId' is missing in type '{ readonly UserId: "UserId"; }' but required in type '{ readonly ProductId: "ProductId"; }'.ts(2345)
28
*/
The error indicates that while both types use branding, the unique values associated with the branding fields ("ProductId" and "UserId") ensure they remain distinct and non-interchangeable.
Generalizing Branded Types
To enhance the versatility and reusability of branded types, they can be generalized using a standardized approach:
1
const
constBrandTypeId:typeof BrandTypeId
BrandTypeId:uniquesymbol=
var Symbol:SymbolConstructor
Symbol.
SymbolConstructor.for(key: string): symbol
Returns a Symbol object from the global symbol registry matching the given key if found.
Otherwise, returns a new symbol with this key.
@param ― key key to search for.
for("effect/Brand")
2
3
// Create a generic Brand interface using a unique identifier
4
interface
interfaceBrand<inoutIDextendsstring|symbol>
Brand<inout
function (typeparameter) IDinBrand<inoutIDextendsstring|symbol>
IDextendsstring|symbol> {
5
readonly [
constBrandTypeId:typeof BrandTypeId
BrandTypeId]: {
6
readonly [
function (typeparameter) id
idin
function (typeparameter) IDinBrand<inoutIDextendsstring|symbol>
ID]:
function (typeparameter) IDinBrand<inoutIDextendsstring|symbol>
ID
7
}
8
}
9
10
// Define a ProductId type branded with a unique identifier
11
type
typeProductId=number&Brand<"ProductId">
ProductId=number&
interfaceBrand<inoutIDextendsstring|symbol>
Brand<"ProductId">
12
13
// Define a UserId type branded similarly
14
type
typeUserId=number&Brand<"UserId">
UserId=number&
interfaceBrand<inoutIDextendsstring|symbol>
Brand<"UserId">
This design allows any type to be branded using a unique identifier, either a string or symbol.
Here’s how you can utilize the Brand interface, which is readily available from the Brand module, eliminating the need to craft your own implementation:
Example (Using the Brand Interface from the Brand Module)
1
import { Brand } from"effect"
2
3
// Define a ProductId type branded with a unique identifier
4
typeProductId=number&Brand.Brand<"ProductId">
5
6
// Define a UserId type branded similarly
7
typeUserId=number&Brand.Brand<"UserId">
However, creating instances of these types directly leads to an error because the type system expects the brand structure:
Example (Direct Assignment Error)
1
const
constBrandTypeId:typeof BrandTypeId
BrandTypeId:uniquesymbol=
var Symbol:SymbolConstructor
Symbol.
SymbolConstructor.for(key: string): symbol
Returns a Symbol object from the global symbol registry matching the given key if found.
Otherwise, returns a new symbol with this key.
@param ― key key to search for.
for("effect/Brand")
2
3
interface
interfaceBrand<inoutKextendsstring|symbol>
Brand<inout
function (typeparameter) KinBrand<inoutKextendsstring|symbol>
Kextendsstring|symbol> {
4
readonly [
constBrandTypeId:typeof BrandTypeId
BrandTypeId]: {
5
readonly [
function (typeparameter) k
kin
function (typeparameter) KinBrand<inoutKextendsstring|symbol>
K]:
function (typeparameter) KinBrand<inoutKextendsstring|symbol>
K
6
}
7
}
8
9
type
typeProductId=number&Brand<"ProductId">
ProductId=number&
interfaceBrand<inoutKextendsstring|symbol>
Brand<"ProductId">
10
11
// @ts-expect-error
12
const
constid:ProductId
id:
typeProductId=number&Brand<"ProductId">
ProductId=1
13
/*
14
Type 'number' is not assignable to type 'ProductId'.
15
Type 'number' is not assignable to type 'Brand<"ProductId">'.ts(2322)
16
*/
You cannot directly assign a number to ProductId. The Brand module provides utilities to correctly construct values of branded types.
Constructing Branded Types
The Brand module provides two main functions for creating branded types: nominal and refined.
nominal
The Brand.nominal function is designed for defining branded types that do not require runtime validations.
It simply adds a type tag to the underlying type, allowing us to distinguish between values of the same type but with different meanings.
Nominal branded types are useful when we only want to create distinct types for clarity and code organization purposes.
Example (Defining Distinct Identifiers with Nominal Branding)
This function returns a Brand.Constructor that does not apply any runtime checks, it just returns the provided value.
It can be used to create nominal types that allow distinguishing between two values of the same type but with different meanings.
This function returns a Brand.Constructor that does not apply any runtime checks, it just returns the provided value.
It can be used to create nominal types that allow distinguishing between two values of the same type but with different meanings.
If you also want to perform some validation, see
refined
.
@example
import { Brand } from"effect"
typeUserId=number&Brand.Brand<"UserId">
constUserId= Brand.nominal<UserId>()
assert.strictEqual(UserId(1), 1)
@since ― 2.0.0
nominal<
typeProductId=number&Brand.Brand<"ProductId">
ProductId>()
18
19
const
constgetProductById: (id:ProductId) =>void
getProductById= (
id: ProductId
id:
typeProductId=number&Brand.Brand<"ProductId">
ProductId) => {
20
// Logic to retrieve product
21
}
Attempting to assign a non-ProductId value will result in a compile-time error:
This function returns a Brand.Constructor that does not apply any runtime checks, it just returns the provided value.
It can be used to create nominal types that allow distinguishing between two values of the same type but with different meanings.
This function returns a Brand.Constructor that does not apply any runtime checks, it just returns the provided value.
It can be used to create nominal types that allow distinguishing between two values of the same type but with different meanings.
Constructs a branded type from a value of type A, throwing an error if
the provided A is not valid.
UserId(1))
33
/*
34
Argument of type 'UserId' is not assignable to parameter of type 'ProductId'.
35
Type 'UserId' is not assignable to type 'Brand<"ProductId">'.
36
Types of property '[BrandTypeId]' are incompatible.
37
Property 'ProductId' is missing in type '{ readonly UserId: "UserId"; }' but required in type '{ readonly ProductId: "ProductId"; }'.ts(2345)
38
*/
refined
The Brand.refined function enables the creation of branded types that include data validation. It requires a refinement predicate to check the validity of input data against specific criteria.
When the input data does not meet the criteria, the function uses Brand.error to generate a BrandErrors data type. This provides detailed information about why the validation failed.
Example (Creating a Branded Type with Validation)
1
import {
import Brand
Brand } from"effect"
2
3
// Define a branded type 'Int' to represent integer values
4
type
typeInt=number&Brand.Brand<"Int">
Int=number&
import Brand
Brand.
interfaceBrand<inoutKextendsstring|symbol>
A generic interface that defines a branded type.
@since ― 2.0.0
@since ― 2.0.0
Brand<"Int">
5
6
// Define the constructor using 'refined' to enforce integer values
Returns a Brand.Constructor that can construct a branded type from an unbranded value using the provided refinement
predicate as validation of the input data.
If you don't want to perform any validation but only distinguish between two values of the same type but with different meanings,
see
nominal
.
@param ― refinement - The refinement predicate to apply to the unbranded value.
@param ― onFailure - Takes the unbranded value that did not pass the refinement predicate and returns a BrandErrors.
@example
import { Brand } from"effect"
typeInt=number&Brand.Brand<"Int">
constInt= Brand.refined<Int>(
(n) => Number.isInteger(n),
(n) => Brand.error(`Expected ${n} to be an integer`)
)
assert.strictEqual(Int(1), 1)
assert.throws(() =>Int(1.1))
@since ― 2.0.0
refined<
typeInt=number&Brand.Brand<"Int">
Int>(
8
// Validation to ensure the value is an integer
9
(
n: number
n) =>
var Number:NumberConstructor
An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.
Returns a Brand.Constructor that can construct a branded type from an unbranded value using the provided refinement
predicate as validation of the input data.
If you don't want to perform any validation but only distinguish between two values of the same type but with different meanings,
see
nominal
.
@param ― refinement - The refinement predicate to apply to the unbranded value.
@param ― onFailure - Takes the unbranded value that did not pass the refinement predicate and returns a BrandErrors.
@example
import { Brand } from"effect"
typeInt=number&Brand.Brand<"Int">
constInt= Brand.refined<Int>(
(n) => Number.isInteger(n),
(n) => Brand.error(`Expected ${n} to be an integer`)
)
assert.strictEqual(Int(1), 1)
assert.throws(() =>Int(1.1))
@since ― 2.0.0
refined<
typeInt=number&Brand.Brand<"Int">
Int>(
6
// Check if the value is an integer
7
(
n: number
n) =>
var Number:NumberConstructor
An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.
Constructs a branded type from a value of type A, throwing an error if
the provided A is not valid.
Int(3)
14
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 and
process.stderr. 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 for
more information.
Example using the global console:
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(newError('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
constname='Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console class:
constout=getStreamSomehow();
consterr=getStreamSomehow();
constmyConsole=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(newError('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
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)
(the arguments are all passed to util.format()).
Returns a Brand.Constructor that can construct a branded type from an unbranded value using the provided refinement
predicate as validation of the input data.
If you don't want to perform any validation but only distinguish between two values of the same type but with different meanings,
see
nominal
.
@param ― refinement - The refinement predicate to apply to the unbranded value.
@param ― onFailure - Takes the unbranded value that did not pass the refinement predicate and returns a BrandErrors.
@example
import { Brand } from"effect"
typeInt=number&Brand.Brand<"Int">
constInt= Brand.refined<Int>(
(n) => Number.isInteger(n),
(n) => Brand.error(`Expected ${n} to be an integer`)
)
assert.strictEqual(Int(1), 1)
assert.throws(() =>Int(1.1))
@since ― 2.0.0
refined<
typeInt=number&Brand.Brand<"Int">
Int>(
6
(
n: number
n) =>
var Number:NumberConstructor
An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.
Returns a Brand.Constructor that can construct a branded type from an unbranded value using the provided refinement
predicate as validation of the input data.
If you don't want to perform any validation but only distinguish between two values of the same type but with different meanings,
see
nominal
.
@param ― refinement - The refinement predicate to apply to the unbranded value.
@param ― onFailure - Takes the unbranded value that did not pass the refinement predicate and returns a BrandErrors.
@example
import { Brand } from"effect"
typeInt=number&Brand.Brand<"Int">
constInt= Brand.refined<Int>(
(n) => Number.isInteger(n),
(n) => Brand.error(`Expected ${n} to be an integer`)
)
assert.strictEqual(Int(1), 1)
assert.throws(() =>Int(1.1))
@since ― 2.0.0
refined<
typeInt=number&Brand.Brand<"Int">
Int>(
6
(
n: number
n) =>
var Number:NumberConstructor
An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.
Returns a Brand.Constructor that can construct a branded type from an unbranded value using the provided refinement
predicate as validation of the input data.
If you don't want to perform any validation but only distinguish between two values of the same type but with different meanings,
see
nominal
.
@param ― refinement - The refinement predicate to apply to the unbranded value.
@param ― onFailure - Takes the unbranded value that did not pass the refinement predicate and returns a BrandErrors.
@example
import { Brand } from"effect"
typeInt=number&Brand.Brand<"Int">
constInt= Brand.refined<Int>(
(n) => Number.isInteger(n),
(n) => Brand.error(`Expected ${n} to be an integer`)
Combines two or more brands together to form a single branded type.
This API is useful when you want to validate that the input data passes multiple brand validators.
@example
import { Brand } from"effect"
typeInt=number&Brand.Brand<"Int">
constInt= Brand.refined<Int>(
(n) => Number.isInteger(n),
(n) => Brand.error(`Expected ${n} to be an integer`)
)
typePositive=number&Brand.Brand<"Positive">
constPositive= Brand.refined<Positive>(
(n) => n >0,
(n) => Brand.error(`Expected ${n} to be positive`)
)
constPositiveInt= Brand.all(Int, Positive)
assert.strictEqual(PositiveInt(1), 1)
assert.throws(() =>PositiveInt(1.1))
@since ― 2.0.0
all(
constInt:Brand.Brand.Constructor<Int>
Int,
constPositive:Brand.Brand.Constructor<Positive>
Positive)
20
21
// Extract the branded type from the PositiveInt constructor