avoid_ad_hoc_left_type
This rule flags an Either, TaskEither or IOEither whose error channel carries a type outside the project’s configured failure hierarchy.
It reports nothing until you configure error_types. What belongs in the error channel is a project decision, so the rule ships with no policy at all.
Why use this rule
Section titled “Why use this rule”flatMap only composes when every step shares one left type. A pipeline that starts TaskEither<String, T> and meets a TaskEither<Failure, T> does not chain — it has to be bridged with mapLeft at every junction.
The usual bridge is (e) => e.toString(), and that is where the damage is done: once the left side is a String, the fold at the boundary can no longer switch on what went wrong. The sealed hierarchy that made failures exhaustive becomes a message, and the UI that wanted to retry on a network error but log out on an auth error has nothing left to branch on.
Naming the hierarchy makes an ad-hoc left type visible where it is introduced, rather than three layers later where someone has to reconcile it.
See also: fpdart: Either
// with error_types: [Failure]TaskEither<String, User> load(String id) => throw UnimplementedError();TaskEither<Failure, User> load(String id) => throw UnimplementedError();Model the hierarchy as a sealed class so the fold can be exhaustive:
sealed class Failure {}class NetworkFailure extends Failure {}class AuthFailure extends Failure {}
void handle(Either<Failure, User> result) { result.match( (failure) => switch (failure) { NetworkFailure() => retry(), AuthFailure() => signOut(), }, (user) => show(user), );}Known limitations
Section titled “Known limitations”Option and Task have no error channel and are never reported.
By default a subtype of a named type is accepted, so a sealed hierarchy works by naming only its root. Set allow_subtypes: false to require the exact type — useful when the point is that every signature spells the same name.
No quick fix is offered: replacing the type means deciding which failure this step produces, which is the design work the rule is asking for.
Options
Section titled “Options”many_lints: rules: avoid_ad_hoc_left_type: error_types: - Failure allow_subtypes: truerules: avoid_ad_hoc_left_type: error_types: - Failure allow_subtypes: true| Option | Type | Default | Description |
|---|---|---|---|
error_types |
list of strings | (none) | The type names allowed in the error channel. Required — the rule is silent without it |
allow_subtypes |
bool | true |
Accept a subtype of a named type, so a sealed hierarchy works by naming only its root |
Configuration
Section titled “Configuration”This rule is in no preset, since it does nothing without configuration. Turn it on by giving it one:
rules: avoid_ad_hoc_left_type: error_types: - FailureTo keep the rule on but skip certain paths, use per-rule exclude.
Related rules
Section titled “Related rules”avoid_bare_await_in_do— Awaiting a raw Future inside a Do block escapes the block’s tracking.avoid_dollar_outside_do_frame— Calling a Do block’s extraction function from a nested callback unwinds through code that cannot handle it.avoid_either_of_future— A Future nested in Either or Option escapes the error channel.avoid_future_of_either— Future<Either> throws away the composition TaskEither already gives you.