Skip to content

Filters

A plain predicate answers one question — does this value pass? — and throws away everything else. When it returns false you are left holding the original value with no record of why it was rejected, and TypeScript has not learned anything new about the values that did pass.

The Filter module replaces that boolean with data. A Filter<Input, Pass, Fail> is a function (input: Input) => Result<Pass, Fail>: success means the value passed (and may have been narrowed or transformed along the way), failure means it was filtered out — and the rejected value is carried in the Result failure channel as ordinary data, not an exception.

import { Filter, Predicate, Result } from "effect"
// A plain predicate: returns a boolean, the rejected value vanishes
const isPositivePredicate = (n: number) => n > 0
isPositivePredicate(5) // => true
isPositivePredicate(-3) // => false (where did -3 go?)
// The same rule as a Filter: the rejected value survives as data
const isPositive = Filter.fromPredicate((n: number) => n > 0)
isPositive(5) // => Result.succeed(5)
isPositive(-3) // => Result.fail(-3) the rejected value is preserved

Because the rejected value is a Result.fail rather than a thrown error, filters compose with combinators (or, zip, compose, …) the same way Result and Option do, instead of forcing you into try/catch.

A refinement predicate makes the win clearer — the passing value is narrowed:

import { Filter, Result } from "effect"
// Refinement: narrows unknown -> string on the pass side
const onlyStrings = Filter.fromPredicate(
(x: unknown): x is string => typeof x === "string"
)
onlyStrings("hello") // => Result.succeed("hello") typed as Result<string, unknown>
onlyStrings(42) // => Result.fail(42)

When the decision needs async work, can error, or depends on services, use FilterEffect — the function returns an Effect<Result<Pass, Fail>, E, R> instead of a bare Result.

import { Effect, Filter, Result } from "effect"
const validateId: Filter.FilterEffect<string, string, string> = (id) =>
Effect.gen(function* () {
const ok = yield* Effect.succeed(id.length > 0)
return ok ? Result.succeed(id) : Result.fail(id)
})

Filter<Input, Pass = Input, Fail = Input> is a function (input) => Result<Pass, Fail>. Success carries the (possibly narrowed/transformed) passing value; failure carries the rejected value.

import { Filter, Result } from "effect"
const positive: Filter.Filter<number> = (n) =>
n > 0 ? Result.succeed(n) : Result.fail(n)
positive(5) // => Result.succeed(5)
positive(-3) // => Result.fail(-3)

FilterEffect<Input, Pass, Fail, E = never, R = never> is the effectful counterpart: (input) => Effect<Result<Pass, Fail>, E, R>. Use it when the filter performs effects, can fail with E, or needs services R.

import { Effect, Filter, Result } from "effect"
const nonEmpty: Filter.FilterEffect<string, string, string> = (s) =>
Effect.sync(() => (s.length > 0 ? Result.succeed(s) : Result.fail(s)))

Builds a Filter from a function returning Result.succeed(pass) / Result.fail(fail). The primary way to write a custom filter, including ones that transform the passing value.

import { Filter, Result } from "effect"
const upperIfNonEmpty = Filter.make((s: string) =>
s.length > 0 ? Result.succeed(s.toUpperCase()) : Result.fail(s)
)
upperIfNonEmpty("hi") // => Result.succeed("HI")
upperIfNonEmpty("") // => Result.fail("")

Builds a FilterEffect from a function returning an Effect<Result<...>>. Use for async checks, error handling, or service access.

import { Effect, Filter, Result } from "effect"
const validate = Filter.makeEffect((id: string) =>
Effect.gen(function* () {
const ok = yield* Effect.succeed(id.length > 0)
return ok ? Result.succeed(id) : Result.fail(id)
})
)
Effect.runSync(validate("abc")) // => Result.succeed("abc")
Effect.runSync(validate("")) // => Result.fail("")

Turns a predicate into a filter that passes the input unchanged on true and fails with the input on false. When given a refinement (x is B), the pass side is narrowed to B.

import { Filter } from "effect"
// Plain predicate: Filter<number>
const positive = Filter.fromPredicate((n: number) => n > 0)
positive(5) // => Result.succeed(5)
positive(-1) // => Result.fail(-1)
// Refinement: narrows the pass type to string
const str = Filter.fromPredicate((x: unknown): x is string => typeof x === "string")
str("ok") // => Result.succeed("ok") pass typed as string
str(1) // => Result.fail(1)

Builds a filter from a function returning an Option. Some(value) passes with value (which can be a transformed result); None fails with the original input.

import { Filter, Option } from "effect"
const parsePort = Filter.fromPredicateOption((s: string) => {
const n = Number(s)
return Number.isInteger(n) && n > 0 && n < 65536 ? Option.some(n) : Option.none()
})
parsePort("8080") // => Result.succeed(8080)
parsePort("nope") // => Result.fail("nope") fails with the original string

Builds a filter that runs a function and passes its return value, failing with the original input if the function throws.

import { Filter } from "effect"
const parseJson = Filter.try((s: string) => JSON.parse(s) as unknown)
parseJson('{"a":1}') // => Result.succeed({ a: 1 })
parseJson("{bad}") // => Result.fail("{bad}") the original string, not the thrown error

Predefined filters that accept an unknown and narrow it to a primitive type on the pass side, failing with the original input otherwise.

Passes string values.

import { Filter } from "effect"
Filter.string("hello") // => Result.succeed("hello")
Filter.string(42) // => Result.fail(42)

Passes number values.

import { Filter } from "effect"
Filter.number(42) // => Result.succeed(42)
Filter.number("42") // => Result.fail("42")

Passes boolean values (true and false).

import { Filter } from "effect"
Filter.boolean(false) // => Result.succeed(false)
Filter.boolean(0) // => Result.fail(0)

Passes bigint primitives. Does not coerce: 1n passes, 1 fails.

import { Filter } from "effect"
Filter.bigint(1n) // => Result.succeed(1n)
Filter.bigint(1) // => Result.fail(1)

Passes symbol values.

import { Filter } from "effect"
const sym = Symbol("id")
Filter.symbol(sym) // => Result.succeed(sym)
Filter.symbol("id") // => Result.fail("id")

Passes Date instances. Uses instanceof Date, so an invalid Date still passes — the timestamp is not validated.

import { Filter } from "effect"
const d = new Date("2026-05-30")
Filter.date(d) // => Result.succeed(d)
Filter.date("2026-05-30") // => Result.fail("2026-05-30")

Passes inputs that are instanceof the given constructor, narrowing the pass side to that instance type.

import { Filter } from "effect"
const isError = Filter.instanceOf(Error)
const e = new TypeError("boom")
isError(e) // => Result.succeed(e) narrowed to Error
isError("boom") // => Result.fail("boom")

Passes inputs whose own has(key) method returns true for the given key. Designed for Set, Map, and similar containers.

import { Filter } from "effect"
const hasUserId = Filter.has("user-1")
const present = new Set(["user-1", "user-2"])
const absent = new Set(["user-3"])
hasUserId(present) // => Result.succeed(present)
hasUserId(absent) // => Result.fail(absent)

Passes only the matching member of a _tag-discriminated union, narrowing the pass side to that variant (ExtractTag) and the fail side to the rest (ExcludeTag). Only _tag is checked, not the other fields. Use the curried tagged<Input>()(tag) form when you want to fix the input union up front.

import { Filter } from "effect"
type Event =
| { readonly _tag: "Click"; readonly x: number }
| { readonly _tag: "Key"; readonly code: string }
// Fix the input type, then pick a tag — pass narrows to the Click variant
const onClick = Filter.tagged<Event>()("Click")
onClick({ _tag: "Click", x: 10 }) // => Result.succeed({ _tag: "Click", x: 10 })
onClick({ _tag: "Key", code: "Enter" }) // => Result.fail({ _tag: "Key", code: "Enter" })

Extracts a nested reason variant from a tagged error: passes when the input’s _tag matches tag and its reason._tag matches reasonTag, succeeding with the inner reason. Otherwise fails with the original input. See Reason errors.

import { Filter } from "effect"
type AppError = {
readonly _tag: "AppError"
readonly reason:
| { readonly _tag: "NotFound"; readonly id: string }
| { readonly _tag: "Forbidden" }
}
const notFound = Filter.reason<AppError>()("AppError", "NotFound")
notFound({ _tag: "AppError", reason: { _tag: "NotFound", id: "u1" } })
// => Result.succeed({ _tag: "NotFound", id: "u1" })
notFound({ _tag: "AppError", reason: { _tag: "Forbidden" } })
// => Result.fail({ _tag: "AppError", reason: { _tag: "Forbidden" } })

Passes inputs structurally equal to the expected value, via Equal.equals. Distinct objects with equal contents pass.

import { Filter } from "effect"
const isOrigin = Filter.equals({ x: 0, y: 0 })
isOrigin({ x: 0, y: 0 }) // => Result.succeed({ x: 0, y: 0 }) different reference, equal contents
isOrigin({ x: 1, y: 0 }) // => Result.fail({ x: 1, y: 0 })

Passes inputs equal to the expected value via JavaScript ===. Objects pass only when they are the same reference, and NaN never passes.

import { Filter } from "effect"
const target = { x: 0, y: 0 }
const isTarget = Filter.equalsStrict(target)
isTarget(target) // => Result.succeed(target) same reference
isTarget({ x: 0, y: 0 }) // => Result.fail({ x: 0, y: 0 }) equal contents, different reference

Tries self, then that; the first success wins. If self passes, its result is returned; otherwise that is run on the same input.

import { Filter } from "effect"
// Accept either a string or a number
const stringOrNumber = Filter.or(Filter.string, Filter.number)
stringOrNumber("hi") // => Result.succeed("hi")
stringOrNumber(42) // => Result.succeed(42)
stringOrNumber(true) // => Result.fail(true)

These run both filters on the same input; if either fails, the combination fails with that failure.

Both filters must pass; the passes are combined into a tuple [PassL, PassR].

import { Filter } from "effect"
const positive = Filter.fromPredicate((n: number) => n > 0)
const even = Filter.fromPredicate((n: number) => n % 2 === 0)
const positiveEvenPair = Filter.zip(positive, even)
positiveEvenPair(4) // => Result.succeed([4, 4])
positiveEvenPair(3) // => Result.fail(3)

Like zip, but combines the two passes with a function f instead of building a tuple.

import { Filter } from "effect"
const str = Filter.string
const positive = Filter.fromPredicate((n: number) => n > 0)
// Both filters run on the same input; here a label string passes both as-is
const labelled = Filter.zipWith(str, str, (a, b) => `${a}/${b}`)
labelled("x") // => Result.succeed("x/x")
labelled(1) // => Result.fail(1)

Both filters must pass; keeps the left filter’s pass value.

import { Filter } from "effect"
const positive = Filter.fromPredicate((n: number) => n > 0)
const even = Filter.fromPredicate((n: number) => n % 2 === 0)
const positiveAndEven = Filter.andLeft(positive, even)
positiveAndEven(4) // => Result.succeed(4) (left pass kept)
positiveAndEven(3) // => Result.fail(3)

Both filters must pass; keeps the right filter’s pass value (useful when the right filter transforms).

import { Filter, Result } from "effect"
const positive = Filter.fromPredicate((n: number) => n > 0)
const doubled = Filter.make((n: number) =>
n > 0 ? Result.succeed(n * 2) : Result.fail(n)
)
const positiveDoubled = Filter.andRight(positive, doubled)
positiveDoubled(5) // => Result.succeed(10) (right pass kept)
positiveDoubled(-1) // => Result.fail(-1)

Runs self, then feeds its pass value into that. The intermediate failure value is preserved: if self fails you get self’s failure, if that fails you get that’s failure.

import { Filter, Result } from "effect"
const nonEmptyUpper = Filter.make((s: string) =>
s.length > 0 ? Result.succeed(s.toUpperCase()) : Result.fail(s)
)
// unknown -> string -> uppercase
const toUpper = Filter.compose(Filter.string, nonEmptyUpper)
toUpper("hi") // => Result.succeed("HI")
toUpper("") // => Result.fail("") failure from the second filter (the intermediate "" string)
toUpper(42) // => Result.fail(42) failure from the first filter

Like compose, but on any failure it fails with the original input rather than the intermediate failure value.

import { Filter, Result } from "effect"
const nonEmptyUpper = Filter.make((s: string) =>
s.length > 0 ? Result.succeed(s.toUpperCase()) : Result.fail(s)
)
const toUpper = Filter.composePassthrough(Filter.string, nonEmptyUpper)
toUpper("hi") // => Result.succeed("HI")
toUpper(42) // => Result.fail(42) original input, same as compose here
// Difference: a value that passes the first filter but fails the second
toUpper("") // => Result.fail("") the ORIGINAL input "", not an intermediate value

Transforms the failure value only, leaving passes untouched. Dual, so it works data-first or in pipe.

import { Filter } from "effect"
const positive = Filter.fromPredicate((n: number) => n > 0)
const withMessage = Filter.mapFail(positive, (n) => `rejected: ${n}`)
withMessage(5) // => Result.succeed(5)
withMessage(-1) // => Result.fail("rejected: -1")

Adapt a Filter back into the plain shapes other APIs expect.

Returns (a) => booleantrue when the filter passes, dropping both payloads.

import { Filter } from "effect"
const isPositive = Filter.toPredicate(Filter.fromPredicate((n: number) => n > 0))
isPositive(5) // => true
isPositive(-1) // => false
;[1, -2, 3].filter(isPositive) // => [1, 3]

Returns (a) => Option<Pass>Some(pass) when the filter passes, None when it fails (the failure value is discarded).

import { Filter } from "effect"
const asOption = Filter.toOption(Filter.string)
asOption("hi") // => Option.some("hi")
asOption(42) // => Option.none()

Returns (a) => Result<Pass, Fail> — preserves both the pass and the failure value. Effectively the identity view of the filter as a plain Result-returning function.

import { Filter } from "effect"
const asResult = Filter.toResult(Filter.number)
asResult(42) // => Result.succeed(42)
asResult("x") // => Result.fail("x")