Problem
Effect.partition(items, f) runs f over every element and returns the two channels as a plain tuple: Effect<[excluded: Array<E>, satisfying: Array<B>], never, R>. A very common follow-up is to immediately gate on the error half — if any element failed, fail with the collected errors; otherwise continue with the successes. Users write this by hand with an Effect.flatMap (or, in gen style, a destructured yield* followed by an if), because partition is the API they know that "collects all failures instead of short-circuiting".
Effect already ships exactly this composite as Effect.validate(items, f). It always evaluates every element, and its type is more precise than what the manual gate can express: it succeeds with Array<B> and fails with NonEmptyArray<E> — the non-emptiness of the error array is carried in the error channel for free, whereas the hand-rolled version fails with a plain Array<E> (the errors.length > 0 fact is erased the moment it leaves the conditional). The rewrite deletes the entire flatMap/conditional scaffold, removes the tuple destructuring (and the chance of swapping the tuple halves — partition puts errors first, which regularly surprises people), and gives downstream catchAll handlers a NonEmptyArray<E> they can index into without a length check.
Bad — compiles cleanly, the rule should flag this
// RULE: partitionThenFailToValidate
// BAD: Effect.partition followed by "fail if any errors, otherwise succeed
// with the results" re-implements Effect.validate, but with a weaker type:
// the failure is a plain Array<E> instead of NonEmptyArray<E>, and the
// [errors, results] tuple order is an easy thing to get backwards.
import { Effect } from "effect"
declare const checkUser: (id: string) => Effect.Effect<string, Error>
declare const userIds: ReadonlyArray<string>
const validatedUsers = Effect.partition(userIds, checkUser).pipe(
Effect.flatMap(([errors, users]) =>
errors.length > 0 ? Effect.fail(errors) : Effect.succeed(users)
)
)
void validatedUsers
// gen-form of the same pattern
const validatedUsersGen = Effect.gen(function*() {
const [errors, users] = yield* Effect.partition(userIds, checkUser)
if (errors.length > 0) {
return yield* Effect.fail(errors)
}
return users
})
void validatedUsersGen
Good
// RULE: partitionThenFailToValidate
// GOOD: Effect.validate is the built-in "run everything, then fail with all
// collected errors" combinator. It succeeds with Array<B> and fails with
// NonEmptyArray<E>, so the non-emptiness the manual length check proved is
// preserved in the error channel instead of erased.
import { Effect } from "effect"
declare const checkUser: (id: string) => Effect.Effect<string, Error>
declare const userIds: ReadonlyArray<string>
// Effect<Array<string>, NonEmptyArray<Error>>
const validatedUsers = Effect.validate(userIds, checkUser)
void validatedUsers
Proposed rule behavior
- Match an
Effect.flatMap (or Effect.andThen with a callback returning an Effect) whose input resolves, via the checker, to a call of Effect.partition, where the callback destructures the tuple into two bindings [errors, results].
- The callback body must be a conditional on the error binding's emptiness —
errors.length > 0, errors.length !== 0, or Arr.isNonEmptyArray(errors) / Arr.isNonEmptyReadonlyArray(errors) — whose truthy branch is Effect.fail(errors) with the error binding passed through unchanged, and whose other branch is Effect.succeed(results) (or just the results binding for andThen).
- Also match the gen form: a destructured
const [errors, results] = yield* Effect.partition(...) followed by if (errors.length > 0) return yield* Effect.fail(errors) and a subsequent use/return of results.
- Use the checker to confirm the destructured tuple really is
partition's [excluded, satisfying] result (element order matters — the rule should key on which binding feeds the length check and which feeds Effect.fail).
- Do NOT flag when the failing branch does anything other than fail with the errors array as-is — mapping/joining the errors into a message, throwing, logging, or otherwise aggregating (the closest real-world near-miss found, opencode's help-snapshots test, joins the failures into a thrown
Error message and consumes the results between the destructure and the check — custom aggregation the rule must leave alone).
- Do NOT flag when the results are used between the partition and the emptiness gate, or when only one side of the tuple is consumed — those are genuine
partition use cases.
- Report suggesting
Effect.validate(items, f), noting the strengthened NonEmptyArray<E> error type; a concurrency option on the partition call carries over unchanged.
Where this came up
No true-positive occurrences found in Effect-TS/effect@c3c7647 or anomalyco/opencode@550d1ff — proposed from the API sweep; both reference codebases are expert-written, so absence there is weak negative signal.
Mined from a per-export sweep of the Effect module (v4): for each exported function, asking what manual pattern it replaces and whether that pattern is statically detectable; grounded against Effect-TS/effect and anomalyco/opencode; deduplicated against implemented tsgo diagnostics and prior rule-proposal issues.
Proposed rule name
partitionThenFailToValidate
Incremental true-positive recount: T3 Code
Reviewed pingdotgg/t3code at 01e05c15268d on 2026-09-14. Scope: tracked first-party TypeScript/JavaScript, including authored tests unless excluded by this proposal; vendored .repos, generated files, dependencies, build output and documentation examples excluded.
- New T3 Code matches: 0. Counts refer to vetted diagnostic source sites, not observed production failures.
- Previous reviewed count bucket:
value:tp-0.
- Confirmed aggregate minimum: 0; label:
value:tp-0. Previously vetted sites remain included; this pass only adds T3 Code.
Review notes. No first-party Effect.partition call.
No new source location met the reviewed trigger and exclusions. Uncertain and version-inapplicable candidates were not added to the count.
Problem
Effect.partition(items, f)runsfover every element and returns the two channels as a plain tuple:Effect<[excluded: Array<E>, satisfying: Array<B>], never, R>. A very common follow-up is to immediately gate on the error half — if any element failed, fail with the collected errors; otherwise continue with the successes. Users write this by hand with anEffect.flatMap(or, in gen style, a destructuredyield*followed by anif), becausepartitionis the API they know that "collects all failures instead of short-circuiting".Effect already ships exactly this composite as
Effect.validate(items, f). It always evaluates every element, and its type is more precise than what the manual gate can express: it succeeds withArray<B>and fails withNonEmptyArray<E>— the non-emptiness of the error array is carried in the error channel for free, whereas the hand-rolled version fails with a plainArray<E>(theerrors.length > 0fact is erased the moment it leaves the conditional). The rewrite deletes the entire flatMap/conditional scaffold, removes the tuple destructuring (and the chance of swapping the tuple halves —partitionputs errors first, which regularly surprises people), and gives downstreamcatchAllhandlers aNonEmptyArray<E>they can index into without a length check.Bad — compiles cleanly, the rule should flag this
Good
Proposed rule behavior
Effect.flatMap(orEffect.andThenwith a callback returning an Effect) whose input resolves, via the checker, to a call ofEffect.partition, where the callback destructures the tuple into two bindings[errors, results].errors.length > 0,errors.length !== 0, orArr.isNonEmptyArray(errors)/Arr.isNonEmptyReadonlyArray(errors)— whose truthy branch isEffect.fail(errors)with the error binding passed through unchanged, and whose other branch isEffect.succeed(results)(or just the results binding forandThen).const [errors, results] = yield* Effect.partition(...)followed byif (errors.length > 0) return yield* Effect.fail(errors)and a subsequent use/return ofresults.partition's[excluded, satisfying]result (element order matters — the rule should key on which binding feeds the length check and which feedsEffect.fail).Errormessage and consumes the results between the destructure and the check — custom aggregation the rule must leave alone).partitionuse cases.Effect.validate(items, f), noting the strengthenedNonEmptyArray<E>error type; aconcurrencyoption on thepartitioncall carries over unchanged.Where this came up
No true-positive occurrences found in Effect-TS/effect@c3c7647 or anomalyco/opencode@550d1ff — proposed from the API sweep; both reference codebases are expert-written, so absence there is weak negative signal.
Mined from a per-export sweep of the Effect module (v4): for each exported function, asking what manual pattern it replaces and whether that pattern is statically detectable; grounded against Effect-TS/effect and anomalyco/opencode; deduplicated against implemented tsgo diagnostics and prior rule-proposal issues.
Proposed rule name
partitionThenFailToValidateIncremental true-positive recount: T3 Code
Reviewed pingdotgg/t3code at
01e05c15268don 2026-09-14. Scope: tracked first-party TypeScript/JavaScript, including authored tests unless excluded by this proposal; vendored.repos, generated files, dependencies, build output and documentation examples excluded.value:tp-0.value:tp-0. Previously vetted sites remain included; this pass only adds T3 Code.Review notes. No first-party Effect.partition call.
No new source location met the reviewed trigger and exclusions. Uncertain and version-inapplicable candidates were not added to the count.