Branded Types
TypeScript is structurally typed: a UserId that is “just a number” is
interchangeable with any other number, so nothing stops you from passing an
OrderId — or a raw 42 — where a UserId is expected. Branded types add a
phantom tag to a value so the compiler treats it as a distinct, nominal type
while it remains an ordinary number (or string) at runtime. The Brand module
gives you the tag and a constructor that produces branded values.
import { Brand } from "effect"
// A branded number type: still a number at runtime, distinct at compile timetype UserId = number & Brand.Brand<"UserId">
// `nominal` builds a constructor that applies no runtime validationconst UserId = Brand.nominal<UserId>()
const getUser = (id: UserId) => `user ${id}`
getUser(UserId(1)) // ok// getUser(1) // compile error: number is not a UserIdBrand.Brand<"UserId"> is the phantom tag, and Brand.nominal<UserId>()
returns a constructor that simply re-labels a plain number as a UserId. The
mislabeled call getUser(1) no longer type-checks, so the two “kinds of number”
can never be confused.
For when and why to reach for brands (the conceptual guide and modeling
trade-offs), see Branded types in the code-style guide.
This page is the exhaustive Brand API reference.
Nominal brands (no validation)
Section titled “Nominal brands (no validation)”Use Brand.nominal when you only want to distinguish values, with no runtime
check. The constructor is the identity at runtime:
import { Brand } from "effect"
type Email = string & Brand.Brand<"Email">type Username = string & Brand.Brand<"Username">
const Email = Brand.nominal<Email>()const Username = Brand.nominal<Username>()
const email = Email("john@example.com")const name = Username("john")
// Email and Username are now incompatible even though both are stringsValidated brands
Section titled “Validated brands”Often a brand carries an invariant: a UserId must be a positive integer, an
Email must contain an @. Brand.make builds a constructor that validates
its input. The filter returns true when the value is valid, or an error
message string when it is not.
import { Brand } from "effect"
type Int = number & Brand.Brand<"Int">
// The filter returns true (valid) or an error message (invalid)const Int = Brand.make<Int>((n) => Number.isInteger(n) || `Expected ${n} to be an integer`)
console.log(Int(42)) // 42
// Int(1.5) // throws BrandError: "Expected 1.5 to be an integer"By default, calling the constructor throws a BrandError on invalid input. To
handle failure as a value instead, the constructor also exposes total variants:
import { Brand } from "effect"
type Int = number & Brand.Brand<"Int">const Int = Brand.make<Int>((n) => Number.isInteger(n) || `Expected ${n} to be an integer`)
// `.option` returns Option<Int>console.log(Int.option(42)) // { _id: 'Option', _tag: 'Some', value: 42 }console.log(Int.option(1.5)) // { _id: 'Option', _tag: 'None' }
// `.result` returns Result<Int, BrandError>console.log(Int.result(1.5))// { _id: 'Result', _tag: 'Failure', failure: BrandError(...) }
// `.is` is a type guardconsole.log(Int.is(42)) // trueconsole.log(Int.is(1.5)) // falseSo every brand constructor gives you four ways to construct, depending on how
you want to treat invalid input: Int(x) (throws), Int.option(x),
Int.result(x), and Int.is(x).
Validating with schema checks
Section titled “Validating with schema checks”When your validation logic already exists as Schema checks, use
Brand.check to reuse them. Multiple checks accumulate, and the failure message
comes from the schema:
import { Brand, Schema } from "effect"
type PositiveInt = number & Brand.Brand<"PositiveInt">
// Combine schema checks into a branded constructorconst PositiveInt = Brand.check<PositiveInt>( Schema.isInt(), Schema.isGreaterThan(0))
console.log(PositiveInt(5)) // 5console.log(PositiveInt.option(-1)) // None — fails the "> 0" checkBrands and Schema
Section titled “Brands and Schema”If a value enters your system through a Schema (decoding JSON from an
HTTP request, say), you usually want the brand applied as part of decoding rather
than as a separate step. The Schema module’s .pipe(Schema.brand("..."))
attaches a brand to a decoded value, so the parsed result is already a
branded type with its invariants checked. Reach for the standalone Brand
module when you are branding values that do not flow through a schema.
Brand reference
Section titled “Brand reference”Everything Brand exports. The two spellings for a branded value are
equivalent — pick whichever reads better:
import { Brand } from "effect"
// Intersection spelling (used above)type A = number & Brand.Brand<"Score">
// Alias spelling — Branded<A, Key> is exactly A & Brand<Key>type B = Brand.Branded<number, "Score">Brand.Brand<Keys>
Section titled “Brand.Brand<Keys>”The phantom tag interface. It contributes no runtime fields (the brand key lives in a unique symbol property), and it can carry multiple keys at once so a single value may hold several brands.
import { Brand } from "effect"
// One keytype UserId = number & Brand.Brand<"UserId">
// Several keys on one value (e.g. produced by Brand.all below)type PositiveInt = number & Brand.Brand<"Positive"> & Brand.Brand<"Int">
const x = 1 as PositiveIntconst useId = (n: UserId) => n// useId(x) // compile error: PositiveInt is not a UserIdBrand.Branded<A, Key>
Section titled “Brand.Branded<A, Key>”A type alias for A & Brand<Key>, slightly more concise when you have a single
brand key.
import { Brand } from "effect"
type Slug = Brand.Branded<string, "Slug"> // ≡ string & Brand.Brand<"Slug">
const Slug = Brand.nominal<Slug>()const s = Slug("hello-world") // => "hello-world" (typed as Slug)Brand.nominal
Section titled “Brand.nominal”Builds a Constructor that applies no runtime validation and returns its
input unchanged — purely for distinguishing values of the same base type.
import { Brand } from "effect"
type OrderId = Brand.Branded<string, "OrderId">const OrderId = Brand.nominal<OrderId>()
console.log(OrderId("ord_42")) // => "ord_42"console.log(OrderId.option("ord_42")) // => Some("ord_42") (always Some)console.log(OrderId.is("anything")) // => true (always true)Brand.make
Section titled “Brand.make”Builds a Constructor from a single validation predicate. The filter returns
true for a valid value, or a string message describing why it is invalid.
import { Brand } from "effect"
type NonEmpty = Brand.Branded<string, "NonEmpty">
const NonEmpty = Brand.make<NonEmpty>((s) => s.length > 0 || "Expected a non-empty string")
console.log(NonEmpty("hi")) // => "hi"console.log(NonEmpty.option("")) // => None// NonEmpty("") // throws BrandError("Expected a non-empty string")Brand.check
Section titled “Brand.check”Builds a Constructor from one or more Schema checks. Checks
accumulate: every check must pass, and the failure message comes from the
schema issue.
import { Brand, Schema } from "effect"
type PositiveInt = Brand.Branded<number, "PositiveInt">
const PositiveInt = Brand.check<PositiveInt>( Schema.isInt(), Schema.isGreaterThan(0))
console.log(PositiveInt(5)) // => 5console.log(PositiveInt.option(2.5)) // => None (fails isInt)console.log(PositiveInt.option(-1)) // => None (fails isGreaterThan(0))console.log(PositiveInt.is(5)) // => trueBrand.all
Section titled “Brand.all”Combines multiple constructors that share the same base type into one
constructor; the combined checks must all pass. Use Brand.Brand.FromConstructor
to name the resulting branded type.
import { Brand, Schema } from "effect"
type Int = Brand.Branded<number, "Int">const Int = Brand.check<Int>(Schema.isInt())
type Positive = Brand.Branded<number, "Positive">const Positive = Brand.check<Positive>(Schema.isGreaterThan(0))
// Carries both brands: number & Brand<"Int"> & Brand<"Positive">const PositiveInt = Brand.all(Int, Positive)type PositiveInt = Brand.Brand.FromConstructor<typeof PositiveInt>
console.log(PositiveInt(5)) // => 5console.log(PositiveInt.option(2.5)) // => None (not an Int)console.log(PositiveInt.option(-3)) // => None (not Positive)If none of the combined constructors carry runtime checks, all behaves like a
nominal constructor.
Brand.Constructor<B>
Section titled “Brand.Constructor<B>”The callable interface shared by every brand constructor. Calling it directly
validates and throws a BrandError; the methods provide non-throwing forms.
import { Brand } from "effect"
type Int = Brand.Branded<number, "Int">const Int = Brand.make<Int>((n) => Number.isInteger(n) || `Expected ${n} to be an integer`)-
Call form —
Int(x): returnsB, throwsBrandErroron invalid input.console.log(Int(42)) // => 42// Int(1.5) // throws BrandError("Expected 1.5 to be an integer") -
.option(x): returnsOption<B>—Someif valid,Noneotherwise.console.log(Int.option(42)) // => Some(42)console.log(Int.option(1.5)) // => None -
.result(x): returnsResult<B, BrandError>—Successif valid,Failurecarrying theBrandErrorotherwise.console.log(Int.result(42)) // => Success(42)console.log(Int.result(1.5)) // => Failure(BrandError("Expected 1.5 to be an integer")) -
.is(x): a type guard returningboolean; narrows the value to the branded type whentrue.const n: number = 42if (Int.is(n)) {// here `n` is narrowed to Int}console.log(Int.is(42)) // => trueconsole.log(Int.is(1.5)) // => false
Brand.BrandError
Section titled “Brand.BrandError”The error produced when construction fails. It is not a JavaScript Error
subclass — it is an error-like model carrying _tag: "BrandError", name,
the underlying schema issue, and a message getter (the rendered issue).
Calling a constructor throws this value, and .result exposes it as the
failure.
import { Brand, Result } from "effect"
type Int = Brand.Branded<number, "Int">const Int = Brand.make<Int>((n) => Number.isInteger(n) || `Expected ${n} to be an integer`)
const r = Int.result(1.5)if (Result.isFailure(r)) { const err = r.failure console.log(err._tag) // => "BrandError" console.log(err.message) // => "Expected 1.5 to be an integer" console.log(String(err)) // => "BrandError(Expected 1.5 to be an integer)"}Brand.Brand namespace (type-level helpers)
Section titled “Brand.Brand namespace (type-level helpers)”The Brand.Brand namespace holds type-level utilities for working with brands
and constructors. The two you reach for most:
Brand.Brand.FromConstructor<C> — extracts the branded type produced by a
constructor C. Handy after Brand.all, or to avoid repeating a brand type.
import { Brand, Schema } from "effect"
const PositiveInt = Brand.check(Schema.isInt(), Schema.isGreaterThan(0))type PositiveInt = Brand.Brand.FromConstructor<typeof PositiveInt>// => number & Brand.Brand<...>Brand.Brand.EnsureCommonBase<Brands> — the constraint Brand.all uses to
verify every constructor shares one base type. If they don’t, it surfaces the
type error "ERROR: All brands should have the same base type".
import { Brand, Schema } from "effect"
const Int = Brand.check(Schema.isInt()) // base: numberconst Lower = Brand.check(Schema.isLowercased()) // base: string
// Brand.all(Int, Lower) // type error: bases (number vs string) differThe remaining helpers are advanced type-level utilities you rarely write by hand:
Brand.Brand.Unbranded<B>— strips the brand to recover the underlying base type (Brand.Brand.Unbranded<UserId>isnumber). This is the type a constructor accepts as input.Brand.Brand.Keys<B>— the union of brand key strings onB.Brand.Brand.Brands<B>— the intersection ofBrand<K>markers carried byB.
See also
Section titled “See also”- Branded types (code-style guide) — when and why to use brands.
- Newtype — wrapping a value in a distinct type when a phantom tag is not enough.
- Schema —
Schema.brand/Schema.fromBrandto apply brands during decoding. - Result and Option — the return
types of
.resultand.option.