Conceptly
← All Concepts
🧩

Algebraic Data Type

DataA closed model of valid shapes

An ADT, or algebraic data type, models a value as one of a closed set of shapes, where each shape carries exactly the data that belongs to it. The word algebraic refers to the two ways those shapes are assembled: products and sums. A product type groups fields that exist together, like a record or struct. A sum type lists alternative variants, where the value can be exactly one of them. ADTs combine those two ideas so the type itself states which states are valid and what data each state contains.

β–ΆArchitecture Diagram

πŸ” Structure

Diagram preview

The interactive diagram loads after the page becomes ready.

Dashed line animations indicate the flow direction of data or requests

Why do you need it?

When state is modeled with a loose object plus a handful of booleans and optional fields, impossible combinations appear easily. A value might claim to be loading while also carrying both data and an error message. The reader has to memorize external rules to know which field combinations are truly allowed, and the logic fills up with defensive checks. The model is no longer helping the code stay correct. ADTs solve that by making impossible states unrepresentable: if a value is Success, it has success data; if it is Error, it has error data; it cannot be both.

Why did this approach emerge?

Functional languages treated precise data modeling as a first-class design tool long before it became common elsewhere. Haskell, Elm, OCaml, F#, Scala, and Rust all pushed the idea that better models simplify downstream logic. That matters especially in modern UI and service code, where the same value often moves through a small state machine: idle, loading, loaded, failed. If those states are represented as one fuzzy object, the branching logic gets complicated fast. For example, const state = { isLoading: true, data: user, error: "timeout" } is a loose object that allows loading, success, and failure information to coexist in one contradictory value. The type does not prevent that impossible combination, so every consumer ends up adding defensive conditionals. ADTs became attractive because they shift correctness work into the model rather than scattering it across conditionals.

How does it work inside?

It helps to read ADTs in two layers. The product layer describes what fields belong together inside one state. A Success state might contain data and receivedAt together. The sum layer describes which states exist at all: Loading | Success | Error, for example. Combining them yields a closed model where every actual value is one variant with one well-defined payload. That closure is the point. Once the variant is known, the caller immediately knows which fields are safe to read and which ones cannot exist in that branch.

In Code

Representing a closed state space in code

type User = { name: string };

type RemoteData<T> =
  | { kind: "idle" }
  | { kind: "loading" }
  | { kind: "success"; data: T; receivedAt: number }
  | { kind: "error"; message: string; retryable: boolean };

const loading: RemoteData<User> = { kind: "loading" };

const success: RemoteData<User> = {
  kind: "success",
  data: { name: "Alice" },
  receivedAt: Date.now(),
};

// Impossible combinations are rejected by the type
// const broken: RemoteData<User> = {
//   kind: "loading",
//   data: { name: "Alice" },
//   message: "timeout",
// };

The `kind` field closes the set of valid states, and each variant carries only the payload that belongs to it. `loading` has no `data`, while `success` must have it, and the type makes that distinction explicit.

Boundaries & Distinctions

ADT and pattern matching are closely related but not identical. ADT is the modeling side: it defines the valid shapes of the data. Pattern matching is the consumption side: it branches on those shapes when the program needs to act. ADT also differs from an ordinary object with optional properties. Loose objects maximize openness; ADTs intentionally reduce openness so the valid state space is small, explicit, and checkable.

When should you use it?

ADTs are especially effective for UI states, async workflows, parser outputs, protocol messages, and domain events -- any place where the possible shapes are known and meaningful. In practice, a type like RemoteData = Idle | Loading | Success Data | Error Message can remove a surprising amount of conditional complexity from a UI. The same applies in backend code: when commands or events carry different payloads depending on their variant, encoding that distinction in the type makes both validation and branching cheaper. ADTs are not decorative FP syntax; they are a way to shrink a messy state space into a model the rest of the system can trust.

UI state modeling -- representing Loading, Success, and Error as one precise typeDomain events -- defining variants like Created, Updated, and Deleted with different payloadsWorkflow state machines -- tying each stage to exactly the fields it is allowed to carryAPI response modeling -- separating success payloads from error payloads in the type itself