Sum types instead of flag combinations. {isLoading, data, error} has 4 impossible states out of 8; a sum type has zero.
Why eight: three independent fields, each either present or absent — 2³. Four of them make sense: nothing started, loading, data arrived, error arrived. The other four — “loading and data at once”, “data and error”, “loading and error”, “everything at once” — are representable in the type, unreachable in meaning, and every code branch has to decide what to do with them. Usually it doesn’t, and the impossible state reaches the user. A sum type removes the question itself: there are exactly four states, because a fifth cannot be expressed.
Java 21
// sealed + record + pattern matching
sealed interface FetchState<T> permits Loading, Loaded, Failed {}
record Loading<T>() implements FetchState<T> {}
record Loaded<T>(T data) implements FetchState<T> {}
record Failed<T>(Exception cause) implements FetchState<T> {}
String render(FetchState<String> s) {
return switch (s) { // compiler demands exhaustiveness
case Loading<String> l -> "spinner";
case Loaded<String>(var data) -> data;
case Failed<String>(var e) -> "error: " + e.getMessage();
};
}Rust
// enum is native; exhaustiveness comes for free
enum FetchState<T> {
Loading,
Loaded(T),
Failed(anyhow::Error),
}
// match without a wildcard: add a variant and it won't compile until handledGo
// no sum types; the closest substitute is a sealed interface + type switch.
// Honest assessment: exhaustiveness is checked by a linter, not the compiler
// (go-check-sumtype / exhaustive)
type fetchState interface{ isFetchState() }
type Loading struct{}
type Loaded struct{ Data []byte }
type Failed struct{ Err error }
func (Loading) isFetchState() {}
func (Loaded) isFetchState() {}
func (Failed) isFetchState() {}Python
# union + match; mypy/pyright check exhaustiveness via assert_never
from dataclasses import dataclass
from typing import assert_never
@dataclass(frozen=True)
class Loading: pass
@dataclass(frozen=True)
class Loaded: data: bytes
@dataclass(frozen=True)
class Failed: cause: Exception
type FetchState = Loading | Loaded | Failed
def render(s: FetchState) -> str:
match s:
case Loading(): return "spinner"
case Loaded(data): return data.decode()
case Failed(cause): return f"error: {cause}"
case _: assert_never(s) # static exhaustiveness