Filters & Branding
A filter (or check) is a constraint attached to a schema that a value must
satisfy after it has the right type. Effect ships a large set of built-in
checks — length, range, pattern, format — and lets you write your own. Attach
them with .check(...).
import { Schema } from "effect"
// A registration form. Each field carries the constraints it must satisfy.const Registration = Schema.Struct({ // String must be non-empty and at most 50 characters. username: Schema.String.check( Schema.isMinLength(1), Schema.isMaxLength(50) ), // Number must be an integer in a valid age range. age: Schema.Number.check( Schema.isInt(), Schema.isBetween({ minimum: 13, maximum: 120 }) ), // String must match an email-ish pattern. email: Schema.String.check(Schema.isPattern(/^[^@\s]+@[^@\s]+$/))})
const user = Schema.decodeUnknownSync(Registration)({ username: "alice", age: 30, email: "alice@example.com"})
console.log(user.username) // => "alice"Attaching checks
Section titled “Attaching checks”There are two equivalent ways to attach checks: the .check(...) method on a
schema, or the standalone Schema.check(...) combinator for use in a pipe. Both
accept one or more checks and run them in order.
import { Schema } from "effect"
// Method formconst A = Schema.Number.check( Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(120))
// Pipe form — identical resultconst B = Schema.Number.pipe( Schema.check( Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(120) ))By default, when multiple checks fail only the first is reported. Pass
{ errors: "all" } to the decoding runner to collect
every failure. A failed check produces a SchemaIssue.Filter wrapping the
input, the failed filter, and the inner issue — see
error formatting for how to read and reshape these.
Every check accepts a trailing annotations argument (a message, title,
description, identifier, etc.) used when the check fails and when deriving
JSON Schema / Arbitrary metadata.
import { Schema } from "effect"
const Username = Schema.String.check( Schema.isMinLength(3, { message: "username must be at least 3 characters" }))Built-in checks
Section titled “Built-in checks”All built-in checks live on the Schema namespace. They are grouped below by
the value they constrain. Each gets a one-line description and a tiny example.
String checks
Section titled “String checks”These operate on string values.
isMinLength
Section titled “isMinLength”At least minLength characters. isNonEmpty() is the isMinLength(1) shortcut.
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isMinLength(3)))("abc") // => "abc"isMaxLength
Section titled “isMaxLength”At most maxLength characters.
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isMaxLength(5)))("abc") // => "abc"isLengthBetween
Section titled “isLengthBetween”Length within [minimum, maximum] (inclusive). Schema.Char is
isLengthBetween(1, 1).
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isLengthBetween(2, 4)))("abc") // => "abc"isNonEmpty
Section titled “isNonEmpty”At least one character. Equivalent to isMinLength(1). Schema.NonEmptyString
bundles this.
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isNonEmpty()))("x") // => "x"isPattern
Section titled “isPattern”Matches a RegExp. The foundation for isUUID, isULID, isBase64, etc.
import { Schema } from "effect"
const Slug = Schema.String.check(Schema.isPattern(/^[a-z0-9-]+$/))Schema.decodeUnknownSync(Slug)("my-slug") // => "my-slug"isStartsWith
Section titled “isStartsWith”Begins with the given prefix.
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isStartsWith("https://")))("https://x") // => "https://x"isEndsWith
Section titled “isEndsWith”Ends with the given suffix.
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isEndsWith(".json")))("a.json") // => "a.json"isIncludes
Section titled “isIncludes”Contains the given substring anywhere.
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isIncludes("@")))("a@b") // => "a@b"isTrimmed
Section titled “isTrimmed”No leading or trailing whitespace. Schema.Trimmed bundles this.
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isTrimmed()))("hi") // => "hi"isLowercased
Section titled “isLowercased”All characters are lowercase.
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isLowercased()))("abc") // => "abc"isUppercased
Section titled “isUppercased”All characters are uppercase.
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isUppercased()))("ABC") // => "ABC"isCapitalized
Section titled “isCapitalized”First character is uppercase.
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isCapitalized()))("Hello") // => "Hello"isUncapitalized
Section titled “isUncapitalized”First character is lowercase.
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isUncapitalized()))("hello") // => "hello"isUUID
Section titled “isUUID”An RFC 4122 UUID. Pass a version (1–8) to require a specific one, or omit it
to accept any version.
import { Schema } from "effect"
const Id = Schema.String.check(Schema.isUUID(4))Schema.decodeUnknownSync(Id)("f47ac10b-58cc-4372-a567-0e02b2c3d479") // => "f47ac10b-..."isULID
Section titled “isULID”A Universally Unique Lexicographically Sortable Identifier (26 Crockford base-32 characters).
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isULID()))("01ARZ3NDEKTSV4RRFFQ69G5FAV")// => "01ARZ3NDEKTSV4RRFFQ69G5FAV"isBase64
Section titled “isBase64”A standard base64-encoded string.
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isBase64()))("aGVsbG8=") // => "aGVsbG8="isBase64Url
Section titled “isBase64Url”A URL-safe base64 string (- and _ instead of + and /).
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isBase64Url()))("aGVsbG8") // => "aGVsbG8"Number checks
Section titled “Number checks”These operate on number values. The comparison checks accept the boundary as
their first argument; isBetween takes an options object.
isGreaterThan
Section titled “isGreaterThan”Strictly greater than (> exclusiveMinimum).
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Number.check(Schema.isGreaterThan(0)))(1) // => 1isGreaterThanOrEqualTo
Section titled “isGreaterThanOrEqualTo”Greater than or equal (>= minimum).
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)))(0) // => 0isLessThan
Section titled “isLessThan”Strictly less than (< exclusiveMaximum).
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Number.check(Schema.isLessThan(10)))(9) // => 9isLessThanOrEqualTo
Section titled “isLessThanOrEqualTo”Less than or equal (<= maximum).
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Number.check(Schema.isLessThanOrEqualTo(10)))(10) // => 10isBetween
Section titled “isBetween”Within a range; boundaries inclusive unless exclusiveMinimum/exclusiveMaximum
are set.
import { Schema } from "effect"
const Port = Schema.Number.check(Schema.isBetween({ minimum: 1, maximum: 65535 }))Schema.decodeUnknownSync(Port)(8080) // => 8080isMultipleOf
Section titled “isMultipleOf”Evenly divisible by divisor.
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Number.check(Schema.isMultipleOf(5)))(15) // => 15A safe integer.
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Number.check(Schema.isInt()))(42) // => 42isInt32
Section titled “isInt32”A 32-bit signed integer (-2147483648–2147483647). A filter group of isInt
plus a range check.
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Number.check(Schema.isInt32()))(1000) // => 1000isUint32
Section titled “isUint32”A 32-bit unsigned integer (0–4294967295).
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Number.check(Schema.isUint32()))(1000) // => 1000isFinite
Section titled “isFinite”A finite number (rejects NaN, Infinity, -Infinity).
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Number.check(Schema.isFinite()))(3.14) // => 3.14BigInt checks
Section titled “BigInt checks”These operate on bigint values; boundaries are bigint literals.
isGreaterThanBigInt
Section titled “isGreaterThanBigInt”Strictly greater than (> exclusiveMinimum).
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.BigInt.check(Schema.isGreaterThanBigInt(0n)))(1n) // => 1nisGreaterThanOrEqualToBigInt
Section titled “isGreaterThanOrEqualToBigInt”Greater than or equal (>= minimum).
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.BigInt.check(Schema.isGreaterThanOrEqualToBigInt(0n)))(0n) // => 0nisLessThanBigInt
Section titled “isLessThanBigInt”Strictly less than (< exclusiveMaximum).
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.BigInt.check(Schema.isLessThanBigInt(10n)))(9n) // => 9nisLessThanOrEqualToBigInt
Section titled “isLessThanOrEqualToBigInt”Less than or equal (<= maximum).
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.BigInt.check(Schema.isLessThanOrEqualToBigInt(10n)))(10n) // => 10nisBetweenBigInt
Section titled “isBetweenBigInt”Within a bigint range, inclusive unless boundaries are marked exclusive.
import { Schema } from "effect"
const Big = Schema.BigInt.check(Schema.isBetweenBigInt({ minimum: 0n, maximum: 100n }))Schema.decodeUnknownSync(Big)(50n) // => 50nBigDecimal checks
Section titled “BigDecimal checks”These operate on BigDecimal values from the BigDecimal module.
isGreaterThanBigDecimal
Section titled “isGreaterThanBigDecimal”Strictly greater than the given BigDecimal.
import { Schema, BigDecimal } from "effect"
const D = Schema.BigDecimal.check(Schema.isGreaterThanBigDecimal(BigDecimal.fromStringUnsafe("1.5")))Schema.decodeUnknownSync(D)(BigDecimal.fromStringUnsafe("2.0")) // => BigDecimal(2.0)isGreaterThanOrEqualToBigDecimal
Section titled “isGreaterThanOrEqualToBigDecimal”Greater than or equal to the given BigDecimal.
import { Schema, BigDecimal } from "effect"
const D = Schema.BigDecimal.check(Schema.isGreaterThanOrEqualToBigDecimal(BigDecimal.fromStringUnsafe("1.5")))Schema.decodeUnknownSync(D)(BigDecimal.fromStringUnsafe("1.5")) // => BigDecimal(1.5)isLessThanBigDecimal
Section titled “isLessThanBigDecimal”Strictly less than the given BigDecimal.
import { Schema, BigDecimal } from "effect"
const D = Schema.BigDecimal.check(Schema.isLessThanBigDecimal(BigDecimal.fromStringUnsafe("10")))Schema.decodeUnknownSync(D)(BigDecimal.fromStringUnsafe("9")) // => BigDecimal(9)isLessThanOrEqualToBigDecimal
Section titled “isLessThanOrEqualToBigDecimal”Less than or equal to the given BigDecimal.
import { Schema, BigDecimal } from "effect"
const D = Schema.BigDecimal.check(Schema.isLessThanOrEqualToBigDecimal(BigDecimal.fromStringUnsafe("10")))Schema.decodeUnknownSync(D)(BigDecimal.fromStringUnsafe("10")) // => BigDecimal(10)isBetweenBigDecimal
Section titled “isBetweenBigDecimal”Within a BigDecimal range, inclusive unless boundaries are marked exclusive.
import { Schema, BigDecimal } from "effect"
const D = Schema.BigDecimal.check( Schema.isBetweenBigDecimal({ minimum: BigDecimal.fromStringUnsafe("0"), maximum: BigDecimal.fromStringUnsafe("1") }))Schema.decodeUnknownSync(D)(BigDecimal.fromStringUnsafe("0.5")) // => BigDecimal(0.5)Date checks
Section titled “Date checks”These operate on Date objects; boundaries are Date values.
isGreaterThanDate
Section titled “isGreaterThanDate”Strictly after the given date.
import { Schema } from "effect"
const After2000 = Schema.Date.check(Schema.isGreaterThanDate(new Date("2000-01-01")))Schema.decodeUnknownSync(After2000)(new Date("2020-01-01")) // => Date(2020-...)isGreaterThanOrEqualToDate
Section titled “isGreaterThanOrEqualToDate”On or after the given date.
import { Schema } from "effect"
const D = Schema.Date.check(Schema.isGreaterThanOrEqualToDate(new Date("2000-01-01")))Schema.decodeUnknownSync(D)(new Date("2000-01-01")) // => Date(2000-...)isLessThanDate
Section titled “isLessThanDate”Strictly before the given date.
import { Schema } from "effect"
const D = Schema.Date.check(Schema.isLessThanDate(new Date("2030-01-01")))Schema.decodeUnknownSync(D)(new Date("2020-01-01")) // => Date(2020-...)isLessThanOrEqualToDate
Section titled “isLessThanOrEqualToDate”On or before the given date.
import { Schema } from "effect"
const D = Schema.Date.check(Schema.isLessThanOrEqualToDate(new Date("2030-01-01")))Schema.decodeUnknownSync(D)(new Date("2030-01-01")) // => Date(2030-...)isBetweenDate
Section titled “isBetweenDate”Within a date range, inclusive unless boundaries are marked exclusive.
import { Schema } from "effect"
const InDecade = Schema.Date.check( Schema.isBetweenDate({ minimum: new Date("2020-01-01"), maximum: new Date("2029-12-31") }))Schema.decodeUnknownSync(InDecade)(new Date("2025-06-01")) // => Date(2025-...)isDateValid
Section titled “isDateValid”A valid Date (rejects new Date("invalid"), whose time is NaN).
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Date.check(Schema.isDateValid()))(new Date("2025-01-01"))// => Date(2025-...)Collection, size, and object checks
Section titled “Collection, size, and object checks”isMinLength / isMaxLength / isLengthBetween also apply to arrays. The size
checks work on anything with a size property (Set, Map), and the property
checks operate on plain objects.
isMinSize
Section titled “isMinSize”A Set/Map (or any { size: number }) with at least minSize entries.
import { Schema } from "effect"
const S = Schema.ReadonlySet(Schema.String).check(Schema.isMinSize(1))Schema.decodeUnknownSync(S)(new Set(["a"])) // => Set(1) { "a" }isMaxSize
Section titled “isMaxSize”At most maxSize entries.
import { Schema } from "effect"
const S = Schema.ReadonlySet(Schema.String).check(Schema.isMaxSize(3))Schema.decodeUnknownSync(S)(new Set(["a", "b"])) // => Set(2) { "a", "b" }isSizeBetween
Section titled “isSizeBetween”Size within [minimum, maximum] (inclusive).
import { Schema } from "effect"
const S = Schema.ReadonlySet(Schema.Number).check(Schema.isSizeBetween(1, 2))Schema.decodeUnknownSync(S)(new Set([1, 2])) // => Set(2) { 1, 2 }isUnique
Section titled “isUnique”An array whose items are all unique under Effect equality.
import { Schema } from "effect"
const Tags = Schema.Array(Schema.String).check(Schema.isUnique())Schema.decodeUnknownSync(Tags)(["a", "b"]) // => ["a", "b"]isMinProperties
Section titled “isMinProperties”An object with at least minProperties own keys (string and symbol keys count).
import { Schema } from "effect"
const Rec = Schema.Record(Schema.String, Schema.Number).check(Schema.isMinProperties(1))Schema.decodeUnknownSync(Rec)({ a: 1 }) // => { a: 1 }isMaxProperties
Section titled “isMaxProperties”An object with at most maxProperties own keys.
import { Schema } from "effect"
const Rec = Schema.Record(Schema.String, Schema.Number).check(Schema.isMaxProperties(2))Schema.decodeUnknownSync(Rec)({ a: 1, b: 2 }) // => { a: 1, b: 2 }isPropertiesLengthBetween
Section titled “isPropertiesLengthBetween”An object whose own-key count is within [minimum, maximum] (inclusive).
import { Schema } from "effect"
const Rec = Schema.Record(Schema.String, Schema.Number).check( Schema.isPropertiesLengthBetween(1, 3))Schema.decodeUnknownSync(Rec)({ a: 1, b: 2 }) // => { a: 1, b: 2 }isPropertyNames
Section titled “isPropertyNames”Every own key validates against the encoded side of the given key schema.
import { Schema } from "effect"
// All keys must be lowercase strings.const Rec = Schema.Record(Schema.String, Schema.Number).check( Schema.isPropertyNames(Schema.String.check(Schema.isLowercased())))Schema.decodeUnknownSync(Rec)({ name: 1 }) // => { name: 1 }String-to-value assertion checks
Section titled “String-to-value assertion checks”These assert that a string is parseable as another representation. They keep
the value a string (they do not transform it) — for actual conversion use the
codecs in transformations.
isStringBigInt
Section titled “isStringBigInt”A signed base-10 integer literal (pattern ^-?\d+$) — Effect’s bigint string
encoding.
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isStringBigInt()))("-42") // => "-42"isStringFinite
Section titled “isStringFinite”A string that represents a finite number.
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isStringFinite()))("3.14") // => "3.14"isStringSymbol
Section titled “isStringSymbol”A string in the Symbol(description) format used by Effect’s symbol encoding.
import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isStringSymbol()))("Symbol(id)") // => "Symbol(id)"Authoring checks
Section titled “Authoring checks”When no built-in fits, build your own. There are three building blocks:
makeFilter (a single predicate), makeFilterGroup (bundle checks together),
and refine (a check that also narrows the type).
makeFilter
Section titled “makeFilter”Schema.makeFilter(predicate, annotations?, abort?) turns a predicate into a
Check. The predicate receives the decoded value and returns a FilterOutput:
undefinedortrue— success.false— generic failure.- a
string— failure with that message. - a
{ path, issue }object — failure pointed at a nested path. - a full
SchemaIssue.Issue, or aReadonlyArrayof any of the above to report several failures at once.
import { Schema } from "effect"
// Cross-field validation: confirmPassword must match password.const PasswordChange = Schema.Struct({ password: Schema.String, confirmPassword: Schema.String}).check( Schema.makeFilter((o) => o.password === o.confirmPassword ? undefined // success : { path: ["confirmPassword"], issue: "passwords must match" } ))
console.log( String( Schema.decodeUnknownExit(PasswordChange)({ password: "hunter2", confirmPassword: "typo" }) ))// => Failure(Cause([Fail(SchemaError: passwords must match// => at ["confirmPassword"])]))A simple boolean predicate plus an expected annotation is the common case:
import { Schema } from "effect"
const Even = Schema.Number.check( Schema.makeFilter((n) => n % 2 === 0, { expected: "an even number" }))Schema.decodeUnknownSync(Even)(4) // => 4makeFilterGroup
Section titled “makeFilterGroup”Schema.makeFilterGroup(checks, annotations?) combines several checks into one,
with shared annotations applied to the group as a whole. This is how isInt32 is
built from isInt plus a range check.
import { Schema } from "effect"
const SmallEven = Schema.makeFilterGroup( [Schema.isMultipleOf(2), Schema.isLessThan(100)], { expected: "a small even number" })
Schema.decodeUnknownSync(Schema.Number.check(SmallEven))(42) // => 42refine
Section titled “refine”Schema.refine(refinement, annotations?) attaches a type-guard predicate that
both validates at runtime and narrows the decoded type. Use it when the
constraint corresponds to a more specific TypeScript type. It returns a
refine<T, S> schema.
import { Schema } from "effect"
// Narrow `string` to the literal "GET" | "POST" with a guard.const Method = Schema.String.pipe( Schema.refine( (s): s is "GET" | "POST" => s === "GET" || s === "POST" ))
type Method = typeof Method.Type // "GET" | "POST"
Schema.decodeUnknownSync(Method)("GET") // => "GET"makeIs* factories
Section titled “makeIs* factories”The ordered comparison checks (isGreaterThan, isBetween, …) are derived from
factories that take an Order.Order instance. Use the same factories to build
comparison checks for your own ordered types — this is exactly how the BigInt,
BigDecimal, and Date variants are produced.
makeIsGreaterThan/makeIsGreaterThanOrEqualTo— build>/>=checks from{ order }.makeIsLessThan/makeIsLessThanOrEqualTo— build</<=checks.makeIsBetween— build a range check; the returned function takes{ minimum, maximum, exclusiveMinimum?, exclusiveMaximum? }.makeIsMultipleOf— build a divisibility check from{ remainder, zero }.
import { Schema, Order } from "effect"
type Money = { readonly cents: number }
// Order Money by its cents field, then derive comparison checks.const order = Order.mapInput(Order.Number, (m: Money) => m.cents)
const isCheaperThan = Schema.makeIsLessThan({ order })const isInPriceRange = Schema.makeIsBetween({ order })
const Affordable = Schema.Struct({ cents: Schema.Number }).check( isInPriceRange({ minimum: { cents: 0 }, maximum: { cents: 5000 } }))
Schema.decodeUnknownSync(Affordable)({ cents: 1999 }) // => { cents: 1999 }When a check fails, the resulting issue is a SchemaIssue.Filter that carries
the input value (actual), the failed filter, and the inner issue. See
error formatting to inspect or reshape these.
Branding
Section titled “Branding”Schema.brand gives a value a nominal type so structurally-identical values
cannot be mixed up — a UserId string can no longer be passed where an
OrderId string is expected. Branding narrows the type but adds no runtime
check on its own, so combine it with the checks that define the brand.
import { Schema } from "effect"
const UserId = Schema.String.pipe( Schema.check(Schema.isNonEmpty()), Schema.brand("UserId"))
type UserId = typeof UserId.Type // string & Brand<"UserId">
const id = Schema.decodeUnknownSync(UserId)("u_1") // typed as UserIdfromBrand
Section titled “fromBrand”If you already have a nominal type defined with the Brand module,
Schema.fromBrand(identifier, ctor) applies that constructor’s checks and its
brand tag to the schema in one step.
import { Schema, Brand } from "effect"
// A Brand.Constructor with its own validation. The filter returns a// FilterOutput: `true`/`undefined` for success, a message string for failure.type Int = number & Brand.Brand<"Int">const Int = Brand.make<Int>((n) => Number.isInteger(n) ? true : `${n} is not an integer`)
const IntSchema = Schema.Number.pipe(Schema.fromBrand("Int", Int))
type IntSchema = typeof IntSchema.Type // number & Brand<"Int">Schema.decodeUnknownSync(IntSchema)(42) // => 42For the broader story on nominal typing — Brand.nominal, Brand.refined, and
when to reach for branded types — see branded types.
With values validated and narrowed, you can also convert between representations — covered in transformations.