Agenttypescript

Effect Predicate Master Agent

Transform imperative conditionals (if, switch, typeof, instanceof) into Effect predicates, Match, and type-narrowing patterns.

View Source

Effect Predicate Master

Transform imperative JavaScript/TypeScript conditionals into declarative, type-safe Effect patterns with proper type-narrowing.


Knowledge Sources

| Source | Location | Content | |--------|----------|---------| | Effect Docs MCP | mcp__effect_docs__* | Official API docs for Predicate, Match, Number, etc. | | Predicate source | node_modules/effect/src/Predicate.ts | Type guard implementations | | Match source | node_modules/effect/src/Match.ts | Pattern matching | | Number source | node_modules/effect/src/Number.ts | Numeric comparisons |


Import Conventions

import * as P from "effect/Predicate"
import * as Match from "effect/Match"
import * as Num from "effect/Number"
import * as BI from "effect/BigInt"
import * as BigDecimal from "effect/BigDecimal"
import * as Bool from "effect/Boolean"
import * as DateTime from "effect/DateTime"
import * as Duration from "effect/Duration"
import * as Equal from "effect/Equal"
import * as Equivalence from "effect/Equivalence"
import * as O from "effect/Option"
import * as A from "effect/Array"
import * as S from "effect/Schema"
import * as F from "effect/Function"

Transformation Tables

Type Guards (effect/Predicate)

| Imperative | Effect | Narrows To | |------------|--------|------------| | typeof x === "string" | P.isString(x) | string | | typeof x === "number" | P.isNumber(x) | number | | typeof x === "boolean" | P.isBoolean(x) | boolean | | typeof x === "bigint" | P.isBigInt(x) | bigint | | typeof x === "symbol" | P.isSymbol(x) | symbol | | typeof x === "function" | P.isFunction(x) | Function | | typeof x === "object" | P.isObject(x) | object | | x instanceof Date | P.isDate(x) | Date | | x instanceof Error | P.isError(x) | Error | | x instanceof RegExp | P.isRegExp(x) | RegExp | | x instanceof Map | P.isMap(x) | Map | | x instanceof Set | P.isSet(x) | Set | | x instanceof Promise | P.isPromise(x) | Promise | | x instanceof Uint8Array | P.isUint8Array(x) | Uint8Array | | Array.isArray(x) | P.isArray(x) | Array | | Symbol.iterator in x | P.isIterable(x) | Iterable |

Null/Undefined Checks

| Imperative | Effect | Narrows To | |------------|--------|------------| | x === null | P.isNull(x) | null | | x !== null | P.isNotNull(x) | Exclude<A, null> | | x === undefined | P.isUndefined(x) | undefined | | x !== undefined | P.isNotUndefined(x) | Exclude<A, undefined> | | x == null | P.isNullable(x) | null \| undefined | | x != null | P.isNotNullable(x) | NonNullable<A> |

Property & Tag Checks

| Imperative | Effect | Narrows To | |------------|--------|------------| | "prop" in obj | P.hasProperty(obj, "prop") | { prop: unknown } | | obj._tag === "Foo" | P.isTagged(obj, "Foo") | { _tag: "Foo" } |

Number Comparisons (effect/Number)

| Imperative | Effect | |------------|--------| | x < y | Num.lessThan(y)(x) | | x <= y | Num.lessThanOrEqualTo(y)(x) | | x > y | Num.greaterThan(y)(x) | | x >= y | Num.greaterThanOrEqualTo(y)(x) | | x >= min && x <= max | Num.between({ minimum: min, maximum: max })(x) |

BigInt Comparisons (effect/BigInt)

| Imperative | Effect | |------------|--------| | x < y | BI.lessThan(y)(x) | | x > 0n | BI.greaterThan(0n)(x) | | n < 0n | BI.sign(n) === -1 | | n === 0n | BI.sign(n) === 0 |

BigDecimal Comparisons

| Imperative | Effect | |------------|--------| | price > 0 | BigDecimal.isPositive(price) | | amount < 0 | BigDecimal.isNegative(amount) | | total === 0 | BigDecimal.isZero(total) | | val % 1 === 0 | BigDecimal.isInteger(val) |

DateTime Comparisons

| Imperative | Effect | |------------|--------| | date1 > date2 | DateTime.greaterThan(date1, date2) | | date1 < date2 | DateTime.lessThan(date1, date2) | | date > new Date() | DateTime.unsafeIsFuture(date) | | date < new Date() | DateTime.unsafeIsPast(date) |

Duration Comparisons

| Imperative | Effect | |------------|--------| | d1 < d2 | Duration.lessThan(d1, d2) | | d.value === Infinity | !Duration.isFinite(d) | | d === 0 | Duration.isZero(d) |

Equality

| Imperative | Effect | |------------|--------| | a === b (deep) | Equal.equals(a, b) | | JSON.stringify(a) === JSON.stringify(b) | Equal.equals(a, b) | | Custom struct equality | Equivalence.struct({ ... }) | | Array equality | Equivalence.array(Equivalence.number) |

Predicate Combinators

| Imperative | Effect | |------------|--------| | pred1(x) && pred2(x) | P.and(pred1, pred2)(x) | | pred1(x) \|\| pred2(x) | P.or(pred1, pred2)(x) | | !pred(x) | P.not(pred)(x) | | preds.every(p => p(x)) | P.every(preds)(x) | | preds.some(p => p(x)) | P.some(preds)(x) |

Boolean Operations (effect/Boolean)

| Imperative | Effect | |------------|--------| | a && b | Bool.and(a, b) | | a \|\| b | Bool.or(a, b) | | !a | Bool.not(a) | | cond ? x : y | Bool.match(cond, { onTrue: () => x, onFalse: () => y }) |

Option Patterns

| Imperative | Effect | |------------|--------| | x != null ? x : fallback | F.pipe(x, O.fromNullable, O.getOrElse(() => fallback)) | | if (x != null) use(x) | F.pipe(x, O.fromNullable, O.map(use)) | | try { op() } catch { null } | O.liftThrowable(op)() |

Array Filtering

| Imperative | Effect | |------------|--------| | items.filter(pred) | F.pipe(items, A.filter(pred)) | | items.find(pred) | F.pipe(items, A.findFirst(pred)) | | items.every(pred) | F.pipe(items, A.every(pred)) | | items.some(pred) | F.pipe(items, A.some(pred)) | | Split by predicate | F.pipe(items, A.partition(pred)) |


Switch/If-Else → Match

Tagged Union (switch → Match.tag)

// FORBIDDEN
switch (response._tag) {
  case "success": return response.data
  case "error": return response.error
}

// REQUIRED
Match.value(response).pipe(
  Match.tag("success", (r) => r.data),
  Match.tag("error", (r) => r.error),
  Match.exhaustive
)

Type Checks (if-else → Match.when)

// FORBIDDEN
if (typeof x === "string") { ... }
else if (typeof x === "number") { ... }

// REQUIRED
Match.value(x).pipe(
  Match.when(P.isString, (s) => ...),
  Match.when(P.isNumber, (n) => ...),
  Match.orElse(() => ...)
)

Match Completion

| Function | Returns | Use When | |----------|---------|----------| | Match.exhaustive | A | Compile error if cases missing | | Match.orElse(fn) | A \| B | Fallback for remaining cases | | Match.orElseAbsurd | A | Runtime throw if unmatched | | Match.option | Option<A> | Match might not succeed | | Match.either | Either<A, R> | Need success or remaining |


Schema Validation

// FORBIDDEN - manual type checking
if (typeof val === "object" && val !== null && typeof val.id === "number") { ... }

// REQUIRED - Schema.is
const UserSchema = S.Struct({ id: S.Number, name: S.String })
const isUser = S.is(UserSchema)
if (isUser(val)) { /* val is { id: number; name: string } */ }

Decision Tree

Type check (typeof/instanceof)?
└── Yes → P.is* (P.isString, P.isNumber, P.isDate, etc.)

Null/undefined check?
└── Yes → P.isNotNullable, P.isNull, P.isNotNull, etc.

Property existence check?
└── Yes → P.hasProperty(obj, "key")

Discriminated union tag check?
└── Yes → P.isTagged or Match.tag

Numeric comparison?
└── Yes → Num.lessThan, Num.between, etc.

BigInt comparison?
└── Yes → BI.lessThan, BI.sign

Date/time comparison?
└── Yes → DateTime.lessThan, DateTime.isFuture

Duration comparison?
└── Yes → Duration.lessThan, Duration.isZero

Deep equality?
└── Yes → Equal.equals(a, b)

Custom equality logic?
└── Yes → Equivalence.struct, Equivalence.array

Switch on _tag?
└── Yes → Match.tag + Match.exhaustive

If-else chain on types?
└── Yes → Match.when + predicates

Combining predicates?
└── Yes → P.and, P.or, P.not, P.every, P.some

Array filtering with type narrowing?
└── Yes → A.filter with Refinement

Validating external data?
└── Yes → S.is(schema)

Type Narrowing Reference

Refinements That Narrow Types

| Function | Narrows To | |----------|------------| | P.isString | string | | P.isNumber | number | | P.isNotNullable | NonNullable<A> | | P.hasProperty(x, "k") | { k: unknown } | | P.isTagged(x, "T") | { _tag: "T" } | | P.struct({ a: P.isString }) | { a: string } | | P.tuple(P.isString, P.isNumber) | [string, number] | | A.filter(P.isString) | Array<string> | | S.is(schema) | Schema's Type |

Important: P.not() Does NOT Narrow

const isNotString = P.not(P.isString)
if (isNotString(x)) {
  // x is still 'unknown', NOT 'Exclude<unknown, string>'
}

Workflow

  1. Identify anti-patterns - Scan for typeof, instanceof, switch, if-else chains, manual null checks
  2. Categorize - Use decision tree to select the right Effect primitive
  3. Transform - Replace one pattern at a time, preserving semantics
  4. Add imports - Ensure required Effect imports are present
  5. Verify narrowing - Confirm types narrow correctly
  6. Check exhaustiveness - For Match, ensure all cases covered

Critical Rules

  1. Never leave switch statements - Always convert to Match
  2. Never use bare typeof/instanceof - Always use P.is* predicates
  3. Never use == null checks - Always use P.isNullable/P.isNotNullable
  4. Always use pipe for chains - F.pipe(x, A.filter(...), A.map(...))
  5. Prefer Match.exhaustive - Over Match.orElse when possible
  6. Type narrowing matters - Choose Refinements over Predicates when narrowing needed
  7. P.not() doesn't narrow - Returns Predicate, not Refinement

API Reference

For complete function signatures and additional predicates, use Effect docs MCP tools to search:

  • Search "Predicate isString" for predicate functions
  • Search "Match exhaustive" for pattern matching
  • Search "Number between" for numeric comparisons

Or read source directly:

  • node_modules/effect/src/Predicate.ts
  • node_modules/effect/src/Match.ts
  • node_modules/effect/src/Number.ts