Skip to content

Filters

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 representative selection — all live on the Schema namespace:

| Category | Checks | | ----------- | -------------------------------------------------------------------------------------------- | | Length/size | isMinLength, isMaxLength, isLengthBetween, isNonEmpty | | Numbers | isGreaterThan, isGreaterThanOrEqualTo, isLessThan, isLessThanOrEqualTo, isBetween, isInt, isMultipleOf | | Strings | isPattern, isStartsWith, isEndsWith, isUUID, isTrimmed |

import { Schema } from "effect"
const Port = Schema.Number.check(Schema.isBetween({ minimum: 1, maximum: 65535 }))
const Slug = Schema.String.check(Schema.isPattern(/^[a-z0-9-]+$/))
const Id = Schema.String.check(Schema.isUUID(4))

For one-off predicates, Schema.makeFilter builds a check from a function. The predicate receives the decoded value and returns a FilterIssue describing the failure (or undefined/true for success). You can point the failure at a nested path and even report several issues 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"])]))

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

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

With values validated, you can also convert between representations — covered in transformations.