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.
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.