Enums in Swift — Associated Values, Pattern Matching, and Algebraic Data Types
Swift enums are not just named integers. They are full sum types that can carry data, model state precisely, and let the compiler prove your switch is complete.
Enums as named values — breaking from C
If you have written any C, you already have a mental model of an enum. In C, an enum is just a set of named integers. You write enum Direction { NORTH, SOUTH, EAST, WEST } and the compiler quietly hands each name a number — NORTH is 0, SOUTH is 1, and so on. The names are a convenience for the human; underneath, it is all just int. That means a C enum will happily compare equal to a stray integer, and you can pass 42 where a Direction was expected without a peep from the compiler.
Swift breaks from this heritage on day one. A Swift enum case is its own thing — a distinct value of a distinct type, not a thin disguise over an integer.
enum Direction {
case north
case south
case east
case west
}
let heading = Direction.north
// heading is a Direction, full stop.
// There is no hidden integer to leak out.
So what if we genuinely do want an underlying integer, or a string, or a character? Swift lets us ask for one explicitly, through a raw value. We declare the raw type after the enum name, and Swift conforms the enum to the RawRepresentable protocol.
enum Planet: Int {
case mercury = 1
case venus // 2, auto-incremented
case earth // 3
case mars // 4
}
Planet.earth.rawValue // 3
enum Suit: String {
case hearts, diamonds, clubs, spades
// raw values default to the case names: "hearts", ...
}
Suit.spades.rawValue // "spades"
Notice the difference in attitude. The raw value is something we opt into, and it lives behind .rawValue rather than being the value itself. Going the other way — from a raw value back to a case — is where Swift shows its hand on safety. What should happen if we hand it a number that matches no case?
let p = Planet(rawValue: 3) // Optional(Planet.earth)
let q = Planet(rawValue: 99) // nil — there is no planet 99
The initialiser init?(rawValue:) is failable. It returns an Optional, because the lookup might fail, and Swift refuses to pretend otherwise. Compare that to C, where casting a bad integer to an enum is undefined territory you only discover at runtime. Here the possibility of failure is right there in the type. Already, before we have touched a single advanced feature, the Swift enum is a more honest tool than its C ancestor.
Associated values — enums become sum types
Raw values attach a single fixed constant to each case. But what if a case needs to carry data that varies from one value to the next — data that is only known at runtime? That is where Swift enums take a leap that C never could. Each case can carry its own associated values: a little payload of any types you like.
enum Barcode {
case upc(Int, Int, Int, Int)
case qr(String)
}
var code = Barcode.upc(8, 85909, 51226, 3)
code = .qr("ABCDEFGHIJKLMNOP")
// Same variable, same type — but the .upc case
// carries four Ints and the .qr case carries a String.
This is a profound shift. A Barcode is now "either a UPC with four numbers, or a QR code with a string." It is one type that is the sum of several shapes. In type-theory language this is a sum type, and combined with the structs Swift uses for product types, it makes the Swift enum a genuine algebraic data type.
The lineage here runs deep. Sum types were pioneered by the ML family of languages in the 1970s and made famous by Haskell, where the type
data Maybe a = Nothing | Just a reads almost exactly like a Swift enum. When Chris Lattner and the Swift team designed the language in the early 2010s, they pulled this idea straight out of functional programming and gave it the friendly C-style keyword enum. The name is a historical accident; the substance is pure ADT.
You have almost certainly used this without noticing. Swift's
Result type is nothing more than an enum with two payload-carrying cases.
enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
func loadUser() -> Result<User, NetworkError> {
// ...
return .success(user)
// or: return .failure(.timedOut)
}
One value that is either a success carrying data or a failure carrying an error. That is a sum type doing real work, and it is the same machinery as our humble Barcode.
Pattern matching — switch and exhaustiveness
An enum that carries data is only half the story. We need a way to ask "which case is this, and what is inside it?" In Swift that question is answered with switch, and switching over an enum is where the safety guarantees really land. A switch over an enum must be exhaustive: it has to handle every case, or the code will not compile.
enum Direction { case north, south, east, west }
func arrow(for d: Direction) -> String {
switch d {
case .north: return "↑"
case .south: return "↓"
case .east: return "→"
case .west: return "←"
}
// No default needed — the compiler can see all four cases.
}
Why is exhaustiveness such a win? Because it turns a runtime surprise into a compile-time error. Add a fifth case,
case up, and every switch that does not yet handle it stops compiling. The compiler becomes a checklist, walking you to each place that needs attention. A stray default: would silence that checklist, so reach for it sparingly.
When a case carries associated values, the switch can bind them to local constants in the same breath. The neat form is
case let, which binds every payload at once.
switch barcode {
case let .upc(system, manufacturer, product, check):
print("UPC: \(system) \(manufacturer) \(product) \(check)")
case let .qr(text):
print("QR: \(text)")
}
We can also add a where clause to a case, so it only matches when an extra condition holds. The cases are tried top to bottom, just like the order they are written.
switch result {
case let .success(value) where value > 100:
print("Big win: \(value)")
case let .success(value):
print("Got \(value)")
case let .failure(error):
print("Failed: \(error)")
}
Binding and guarding together let a single switch read almost like a sentence: "on success with a value over one hundred, do this; on any other success, do that; on failure, handle the error." The data and the decision live in one place, and the compiler keeps the whole thing honest.
Methods, computed properties, and mutating self
Here is something that surprises people coming from C: a Swift enum can have methods. It can also have computed properties, static members, initialisers, and conform to protocols. An enum is a first-class type, not a bag of constants. The one thing it cannot have is a stored property — there is nowhere to put per-instance storage beyond the case and its associated values.
enum Direction {
case north, south, east, west
// A computed property — fine, nothing stored.
var symbol: String {
switch self {
case .north: return "↑"
case .south: return "↓"
case .east: return "→"
case .west: return "←"
}
}
// A method.
func degrees() -> Int {
switch self {
case .north: return 0
case .east: return 90
case .south: return 180
case .west: return 270
}
}
}
Direction.east.symbol // "→"
Direction.east.degrees() // 90
Try to add var visits = 0 and the compiler stops you: "enums must not contain stored properties." That restriction is the price of an enum being a closed set of cases — every value of the type is one of the cases, and nothing more.
Now for the genuinely clever part. A method marked
mutating can reassign self to a different case. Because an enum value is simply "which case am I," changing the case is the whole mutation. This gives us a state machine in a handful of lines.
enum TrafficLight {
case red, green, amber
mutating func advance() {
switch self {
case .red: self = .green
case .green: self = .amber
case .amber: self = .red
}
}
}
var light = TrafficLight.red
light.advance() // light is now .green
light.advance() // light is now .amber
light.advance() // back to .red
Read advance() and the entire transition table is right there. There is no separate "current state" variable, no integer to keep in sync, no chance of landing in an invalid state — the type itself only permits the three cases. This is one of the most pleasing patterns enums enable, and we will lean on it constantly when modelling UI and network state.
Indirect enums — recursive data structures
Sum types are wonderful for finite shapes, but some data is recursive: a tree whose branches are smaller trees, a linked list whose tail is another list, an arithmetic expression built from smaller expressions. Can an enum case hold a value of its own type? Let's try the obvious thing.
enum Expr {
case value(Int)
case add(Expr, Expr) // error!
}
Swift rejects this with "recursive enum 'Expr' is not marked 'indirect'." The reason is layout. As we will see in a moment, an enum is laid out inline, sized to its largest case. But a case that contains the enum itself would need infinite size — the box would have to be big enough to hold a copy of itself, which is impossible. The fix is the indirect keyword, which tells Swift to store the recursive payload behind a pointer to a heap-allocated box rather than inline.
indirect enum Expr {
case value(Int)
case add(Expr, Expr)
case multiply(Expr, Expr)
}
// Represent 1 + (2 + 3)
let tree: Expr = .add(.value(1), .add(.value(2), .value(3)))
func evaluate(_ e: Expr) -> Int {
switch e {
case let .value(n): return n
case let .add(l, r): return evaluate(l) + evaluate(r)
case let .multiply(l, r): return evaluate(l) * evaluate(r)
}
}
evaluate(tree) // 6
You can mark the whole enum
indirect, as above, or mark only the individual cases that need it: indirect case add(Expr, Expr). The latter is a little more frugal, because non-recursive cases like .value stay inline and only the recursive ones pay for a heap allocation. Either way, the same trick powers a binary tree, a JSON-like nested value, or a singly linked list:
indirect enum List<Element> {
case empty
case node(Element, List<Element>)
}
let nums: List<Int> = .node(1, .node(2, .node(3, .empty)))
With one keyword, an enum graduates from describing flat alternatives to describing arbitrarily deep, recursive structures — and the exhaustive switch still keeps us honest about handling the base case.
CaseIterable and enums as namespaces
Often we want the full list of an enum's cases — to populate a picker, to run a test over every option, to build a lookup table. Writing that array by hand is tedious and goes stale the moment someone adds a case. Swift can synthesise it for us. Conform an enum to CaseIterable and the compiler generates a static allCases collection automatically.
enum Suit: CaseIterable {
case hearts, diamonds, clubs, spades
}
Suit.allCases.count // 4
for suit in Suit.allCases {
print(suit) // hearts, diamonds, clubs, spades
}
There is a quiet rule worth knowing: the compiler will only synthesise allCases for enums whose cases have no associated values. That makes sense, because a case like .qr(String) stands for infinitely many possible values — there is no finite list to hand back. For simple, payload-free enums, though, CaseIterable is free and it never drifts out of date.
Now for an idiom that looks odd at first. An enum with no cases at all is perfectly legal, and surprisingly useful. Because it has no cases, it can never be instantiated — there is no value of the type. We exploit that to build a pure namespace: a tidy bag of static members that nobody can accidentally create an instance of.
enum Math {
static let pi = 3.14159265358979
static let e = 2.71828182845905
static func square(_ x: Double) -> Double { x * x }
}
Math.pi // 3.14159...
Math.square(4) // 16
// let m = Math() // error: no accessible initialisers
A struct could hold these statics too, but a struct comes with a free memberwise initialiser, so someone could write Math() and create a pointless empty value. A caseless enum slams that door shut: zero cases means zero instances. This is why you will see Swift codebases use enums purely as namespaces for constants and helper functions. The Swift standard library itself does this in places — it is the recognised, intentional way to say "this type is a container for static members and nothing else."
Trivia and the bigger picture
Let's zoom out and connect the threads, with a few facts worth tucking away. The first is that two of Swift's most-used types are themselves just enums. Optional<Wrapped> — the thing behind every String? — is an enum with two cases:
enum Optional<Wrapped> {
case none
case some(Wrapped)
}
// "nil" is sugar for .none; a value is .some(value).
And Result, which we met earlier, is the same idea with success and failure. So every time you unwrap an optional or pattern-match a result, you are pattern-matching an enum. The feature is not a niche tool bolted on the side; it is woven into the spine of the language.
How is all of this stored? Under the hood a Swift enum is a small struct-like value: a discriminator tag that records which case it is, plus a payload region sized to hold the largest case. Only one case occupies the payload at any moment, and the tag tells the runtime how to read those bytes. Swift is even cleverer than that — for some enums it can pack the tag into spare bit patterns of the payload, so an
Optional<SomeReferenceType> often costs not a single extra byte.
It is worth holding three comparisons in mind. Against a C enum, which is a plain integer with no payload and no exhaustiveness, the Swift enum is a different beast entirely — distinct type, associated values, methods, the lot. Against Dart's enhanced enums, the picture is more nuanced: Dart enums can carry data too, but every Dart enum value has the same set of fields, whereas each Swift case can carry a completely different payload — that is the sum-type difference, and it is why Dart added sealed classes to cover the cases enums could not. And against a Swift struct: a struct is a product type that bundles stored properties together, while an enum is a sum type that holds exactly one of several alternatives and, crucially, has no stored properties of its own.
So the next time you reach for
enum in Swift, remember you are not declaring a list of integers. You are declaring a precise set of possibilities — a sum type with the full lineage of ML and Haskell behind it, the compiler standing guard over every switch, and the ability to model exactly the states your program can be in and no others.
Test your understanding
7 questions
Seven questions covering raw values, associated values, pattern matching, mutating methods, indirect enums, CaseIterable, and the bigger picture.