Skip to content

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"

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 form
const A = Schema.Number.check(
Schema.isGreaterThanOrEqualTo(0),
Schema.isLessThanOrEqualTo(120)
)
// Pipe form — identical result
const 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" })
)

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.

These operate on string values.

At least minLength characters. isNonEmpty() is the isMinLength(1) shortcut.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isMinLength(3)))("abc") // => "abc"

At most maxLength characters.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isMaxLength(5)))("abc") // => "abc"

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"

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"

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"

Begins with the given prefix.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isStartsWith("https://")))("https://x") // => "https://x"

Ends with the given suffix.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isEndsWith(".json")))("a.json") // => "a.json"

Contains the given substring anywhere.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isIncludes("@")))("a@b") // => "a@b"

No leading or trailing whitespace. Schema.Trimmed bundles this.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isTrimmed()))("hi") // => "hi"

All characters are lowercase.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isLowercased()))("abc") // => "abc"

All characters are uppercase.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isUppercased()))("ABC") // => "ABC"

First character is uppercase.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isCapitalized()))("Hello") // => "Hello"

First character is lowercase.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isUncapitalized()))("hello") // => "hello"

An RFC 4122 UUID. Pass a version (18) 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-..."

A Universally Unique Lexicographically Sortable Identifier (26 Crockford base-32 characters).

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isULID()))("01ARZ3NDEKTSV4RRFFQ69G5FAV")
// => "01ARZ3NDEKTSV4RRFFQ69G5FAV"

A standard base64-encoded string.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isBase64()))("aGVsbG8=") // => "aGVsbG8="

A URL-safe base64 string (- and _ instead of + and /).

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isBase64Url()))("aGVsbG8") // => "aGVsbG8"

These operate on number values. The comparison checks accept the boundary as their first argument; isBetween takes an options object.

Strictly greater than (> exclusiveMinimum).

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Number.check(Schema.isGreaterThan(0)))(1) // => 1

Greater than or equal (>= minimum).

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)))(0) // => 0

Strictly less than (< exclusiveMaximum).

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Number.check(Schema.isLessThan(10)))(9) // => 9

Less than or equal (<= maximum).

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Number.check(Schema.isLessThanOrEqualTo(10)))(10) // => 10

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) // => 8080

Evenly divisible by divisor.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Number.check(Schema.isMultipleOf(5)))(15) // => 15

A safe integer.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Number.check(Schema.isInt()))(42) // => 42

A 32-bit signed integer (-21474836482147483647). A filter group of isInt plus a range check.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Number.check(Schema.isInt32()))(1000) // => 1000

A 32-bit unsigned integer (04294967295).

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Number.check(Schema.isUint32()))(1000) // => 1000

A finite number (rejects NaN, Infinity, -Infinity).

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.Number.check(Schema.isFinite()))(3.14) // => 3.14

These operate on bigint values; boundaries are bigint literals.

Strictly greater than (> exclusiveMinimum).

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.BigInt.check(Schema.isGreaterThanBigInt(0n)))(1n) // => 1n

Greater than or equal (>= minimum).

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.BigInt.check(Schema.isGreaterThanOrEqualToBigInt(0n)))(0n) // => 0n

Strictly less than (< exclusiveMaximum).

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.BigInt.check(Schema.isLessThanBigInt(10n)))(9n) // => 9n

Less than or equal (<= maximum).

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.BigInt.check(Schema.isLessThanOrEqualToBigInt(10n)))(10n) // => 10n

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) // => 50n

These operate on BigDecimal values from the BigDecimal module.

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)

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)

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)

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)

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)

These operate on Date objects; boundaries are Date values.

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

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

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

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

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

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

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.

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" }

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" }

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 }

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"]

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 }

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 }

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 }

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 }

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.

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"

A string that represents a finite number.

import { Schema } from "effect"
Schema.decodeUnknownSync(Schema.String.check(Schema.isStringFinite()))("3.14") // => "3.14"

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)"

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

Schema.makeFilter(predicate, annotations?, abort?) turns a predicate into a Check. The predicate receives the decoded value and returns a FilterOutput:

  • undefined or true — 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 a ReadonlyArray of 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) // => 4

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) // => 42

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"

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.

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 UserId

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) // => 42

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